pjfanning opened a new pull request, #1442:
URL: https://github.com/apache/poi/pull/1442
Three places run regular expressions over file content that backtrack
quadratically on crafted input. Each fix keeps the same accept/reject results
and the same captured groups.
## Repro and cause
**`sl.draw.geom.Context.DOUBLE_PATTERN`** (the `Double.valueOf` javadoc
regex, used by `getValue` on guide formula operands and path values from PPTX
`a:custGeom`). On `"1" * n + "x"`, the digits can be split between
`(\p{Digit}+)` and the optional `(\p{Digit}+)?` after the optional `.` in every
possible way before the match fails.
**`hwpf.converter.AbstractWordConverter`** field patterns:
- `PATTERN_HYPERLINK_EXTERNAL` `^[ \t\r\n]*HYPERLINK "(.*)".*$` on
`HYPERLINK "` + `"` * n + `"\nx`: every `"` is tried as the closing quote, and
each time `.*$` scans to the line break.
- `PATTERN_HYPERLINK_LOCAL` `^[ \t\r\n]*HYPERLINK \\l "(.*)"[ ](.*)$` on
`HYPERLINK \l "` + `" ` * n + `\nx`: same problem.
- `PATTERN_PAGEREF` `^[ \t\r\n]*PAGEREF ([^ ]*)[ \t\r\n]*\\h.*$` on `PAGEREF
` + `\t` * n + `x` (`([^ ]*)` and `[ \t\r\n]*` compete for the tabs), and on
`PAGEREF ` + `\h` * n + `\n\n` (every `\h` is retried against a failing `.*$`).
**`hsmf.MAPIMessage.guess7BitEncoding`** (called by `OutlookTextExtractor`),
`content-type:.*?charset=["']?([^;'"]+)["']?` with `matches()` on transport
header lines. On `content-type:` + `charset=a` * k + `;`, every `charset=` is
tried, and each time `[^;'"]+` scans to the trailing `;`.
## Fix
- `Context`: the digit runs are now possessive (`\p{Digit}++`,
`\p{XDigit}++`). Giving digits back can't turn a failed match into a successful
one, because whatever follows each run can't start with a digit, or (for the
first run) the next run just picks up the same digits and ends at the same
place.
- `AbstractWordConverter`: the leading part of each pattern is wrapped in an
atomic group, so only its longest match is kept:
- `^[ \t\r\n]*HYPERLINK "(?>(.*)").*$`
- `^[ \t\r\n]*HYPERLINK \\l "(?>(.*)"[ ])(.*)$`
- `^[ \t\r\n]*PAGEREF (?>([^ ]*)(?: [ \t\r\n]*+)?\\h).*$`
The old patterns used the longest match anyway (greedy). When the trailing
`.*$` fails, it fails because of a line break that every shorter match would
hit as well, so trying them again can't help. In PAGEREF, the whitespace
between `([^ ]*)` and `\h` now has to start with a space. Tabs or line breaks
right after the group would have been taken by `([^ ]*)` itself on an earlier,
longer attempt, so this doesn't change what matches. It removes the
tab-splitting inside the atomic group. The patterns are now package-private so
the tests can reach them.
- `MAPIMessage`: the regex is replaced by `getCharsetFromContentType`, a
package-private static helper that matches in linear time. It checks for an
ASCII case-insensitive `content-type:` prefix. The value ends at the end of the
header, or one character earlier if the last character is a quote, and it must
not contain `;` or a quote. The helper scans the first line for `charset=`,
skips one optional opening quote, and returns the value at the first position
that satisfies those rules. Case folding is ASCII-only, like `CASE_INSENSITIVE`
without `UNICODE_CASE`. For example, `ſ` (U+017F) does not match `s`.
## Equivalence check
Old and new were compared exhaustively (matches/find result plus start/end
of every group):
- `DOUBLE_PATTERN`: all strings up to length 7 over `0 1 a . e x p + d space
\n N U+0660`, about 68M strings, 34,676 accepted, no differences.
- `HYPERLINK_EXTERNAL` and `HYPERLINK_LOCAL`: all bodies up to length 8 over
`" a space \n \r U+0085 U+2028 U+2029` (EXTERNAL) and `" space a` plus the same
line terminators (LOCAL), each after 4 prefixes (none, the matching prefix, a
prefix with leading whitespace/CRLF, the other HYPERLINK prefix). That is 76.7M
strings per pattern, no differences.
- `PAGEREF`: all bodies up to length 7 over `space \t \ h a \n \r U+0085
U+2028` after 5 prefixes. That is 26.9M strings, 329,414 matched, no
differences.
- MAPI helper vs the old regex `group(1)`, two runs, no differences:
- all sequences of up to 6 tokens (`charset=`, `CharSet=`, `charsex=`,
`a`, `;`, `'`, `"`, line terminators, space) after 8 prefixes, including
near-miss prefixes and Unicode case-folding traps. That is 41.8M strings, 2.37M
matched.
- all strings up to length 7 over `c C h s U+017F e t = ; " ' \n x` after
4 prefixes. That is 272M strings, 17.6M matched.
## Timings (old vs new, ms)
| input | old | new |
|---|---|---|
| Context `"1"*5000+"x"` | 6,813 | 14 |
| Context `"1"*20000+"x"` | 145,849 | 3 |
| HYPERLINK external n=20k / 50k | 2,608 / 38,143 | 0 / 0 |
| HYPERLINK local n=20k / 50k | 5,341 / 34,271 | 0 / 0 |
| PAGEREF tabs n=20k / 50k | 9,833 / 55,278 | 6 / 5 |
| PAGEREF `\h` n=20k / 50k | 2,364 / 6,521 | 1 / 0 |
| MAPI k=2k / 5k / 10k `charset=a` | 808 / 3,660 / 15,650 | 8 / 9 / 13 |
The new code takes about 100 ms on 1M-character inputs. The machine was
loaded while these ran, so the absolute numbers are noisy, but the quadratic
growth is clear.
## Tests
New tests:
- `TestFormulaParser.testNumericValues`: number parsing, plus a 50k-digit
key that must be rejected within 10 s.
- `TestWordToHtmlConverter.testFieldPatterns` and
`testFieldPatternsBacktracking`.
- `Test7BitCodepage.testCharsetFromContentType`.
The slowness tests use `assertTimeoutPreemptively(Duration.ofSeconds(10),
...)`. I ran their exact inputs against the old patterns in a standalone
harness, and all six timed out at 10 s. I also checked their positive
assertions against the old patterns.
Targeted runs, all passing:
- `poi`: TestFormulaParser (2), TestPresetGeometries (1)
- `poi-scratchpad`: TestWordToHtmlConverter (24), TestWordToFoConverter (8),
TestWordToTextConverter (168), Test7BitCodepage (2), TestBasics (12, 1 skipped
as before), TestOutlookTextExtractor (8)
- `poi-ooxml`: TestXSLFSimpleShape (7)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]