Skip to content

v0.14.1

Choose a tag to compare

@GitHubNewbie0 GitHubNewbie0 released this 11 Aug 19:37
· 27 commits to main since this release
  • Polynomial regular-expression backtracking in the length core is fixed (CodeQL js/polynomial-redos, alerts #28 and #29 — High). The expressions matching ODF length and percentage lexicals shared the numeric core \d+\.?\d*: because the dot was optional, \d+ and \d* were both applicable to the same run of digits, so an n-digit input produced on the order of n²/2 backtracking paths before the anchored suffix failed. Both are reachable from raw document attribute text, which makes a crafted .odt the direct threat model. The rewrites are language-equivalent — \d+(?:\.\d*)? accepts exactly the same lexicals as \d+\.?\d*, including the trailing-dot form — and are proven so by a differential test that compares the old and new expressions for identical accept/reject decisions and identical capture groups, and separately compares the whole pre-change parsing pipeline against the shipped one. Accepted input is unchanged.
  • The same class in decimal formatting is fixed (alert #30 — moderate). The trailing-zero strip used an expression unanchored at the left, quadratic on long runs of zeros; it is now a character walk. CodeQL traced this one from caller-supplied API arguments rather than from document text, hence the lower severity. Worth recording: the strip's three states are provably unreachable from every public entry point — the rational core normalises to lowest terms, so no computed numerator ends in zero, and the shortest-decimal search returns at the minimal scale, where a trailing zero would contradict minimality. The rewrite removes the cost regardless, and the release adds a sweep asserting the guarantee that makes the strip unreachable: no decimal odf-kit computes ever carries a trailing zero in its fractional part or ends in a bare dot. Values you hand in are unaffected — a lexical such as 1.500cm is still preserved verbatim.
  • Two further sites of the same class are fixed in CSS dimension parsing (htmlToOdt's px and unitless line-height handling). These were found by a manual idiom sweep of the source rather than by CodeQL, which did not flag them, and they sit on the same untrusted-input path as the alerts above. Language-equivalent rewrites; accepted input unchanged.
  • New documented input bound: numeric lexicals longer than 64 characters are rejected. parseOdfValue returns undefined; value-constructing APIs throw an error naming the bound, and that error excerpts the offending text rather than echoing unbounded input back. This is a deliberate deviation from the ODF RNG grammar, whose [0-9]* is unbounded. The bound is derived rather than chosen — from odf-kit's own precision ceilings, the 25-fractional-digit emission search and the 15-significant-digit float boundary — and sits far beyond any observed producer output, where the deepest values seen carry about six digits. It exists to bound parse cost against crafted documents, and it also closes a non-regex cost path in which very long inputs drove BigInt and GCD work with no expression involved.
  • CSS px values with 25 or more fractional digits now degrade instead of converting. This is a narrowing of accepted input, not only a crash fix. Such a value implies an interval narrower than the emission search's finest grid, so before this release roughly a quarter of them threw and the rest converted, decided by the mantissa's digits — behaviour no caller could predict. The threshold is the guaranteed-safe one, derived from the search ceiling and the px-to-pt factor rather than measured, so conversion either succeeds or is declined deterministically. Values at 24 fractional digits and below convert exactly as before; no producer emits at this depth.
  • htmlToOdt no longer throws on CSS lengths it cannot convert. Its documented contract — unconvertible input yields "property not extracted", never an exception — was violated by two lexical classes reachable from crafted CSS: the trailing-dot form (12.px), which the expression accepted but the length core rejected, and values past the emission ceiling. All three rejection classes — malformed shape, over the 64-character bound, and past the emission depth — now degrade, so the property is simply absent from the output and conversion completes. The bound throws at API boundaries, where a programmer can act on the error, and degrades at document boundaries, where attacker-controlled input must not abort a conversion.