diff --git a/CHANGELOG.md b/CHANGELOG.md index 096018b7d88..7b515a102d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai - Security/Discord: add `openclaw security audit` warnings for name/tag-based Discord allowlist entries (DM allowlists, guild/channel `users`, and pairing-store entries), highlighting slug-collision risk while keeping name-based matching supported, and canonicalize resolved Discord allowlist names to IDs at runtime without rewriting config files. Thanks @tdjackey for reporting. - Security/Gateway: block node-role connections when device identity metadata is missing. - Security/Media: enforce inbound media byte limits during download/read across Discord, Telegram, Zalo, Microsoft Teams, and BlueBubbles to prevent oversized payload memory spikes before rejection. This ships in the next npm release. Thanks @tdjackey for reporting. +- Media/Understanding: preserve `application/pdf` MIME classification during text-like file heuristics so PDF uploads use PDF extraction paths instead of being inlined as raw text. (#23191) Thanks @claudeplay2026-byte. - Security/Control UI: block symlink-based out-of-root static file reads by enforcing realpath containment and file-identity checks when serving Control UI assets and SPA fallback `index.html`. This ships in the next npm release. Thanks @tdjackey for reporting. - Security/MSTeams media: enforce allowlist checks for SharePoint reference attachment URLs and redirect targets during Graph-backed media fetches so redirect chains cannot escape configured media host boundaries. This ships in the next npm release. Thanks @tdjackey for reporting. - Security/macOS discovery: fail closed for unresolved discovery endpoints by clearing stale remote selection values, use resolved service host only for SSH target derivation, and keep remote URL config aligned with resolved endpoint availability. (#21618) Thanks @bmendonca3. diff --git a/src/media-understanding/apply.e2e.test.ts b/src/media-understanding/apply.e2e.test.ts index 3c3b40412cd..018e84cd3a5 100644 --- a/src/media-understanding/apply.e2e.test.ts +++ b/src/media-understanding/apply.e2e.test.ts @@ -632,6 +632,38 @@ describe("applyMediaUnderstanding", () => { expect(ctx.Body).not.toContain(" { + const pseudoPdf = Buffer.from("%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n", "utf8"); + const filePath = await createTempMediaFile({ + fileName: "report.pdf", + content: pseudoPdf, + }); + + const cfg: OpenClawConfig = { + ...createMediaDisabledConfig(), + gateway: { + http: { + endpoints: { + responses: { + files: { allowedMimes: ["text/plain"] }, + }, + }, + }, + }, + }; + + const { ctx, result } = await applyWithDisabledMedia({ + body: "", + mediaPath: filePath, + mediaType: "application/pdf", + cfg, + }); + + expect(result.appliedFile).toBe(false); + expect(ctx.Body).toBe(""); + expect(ctx.Body).not.toContain(" { const tsvText = "a\tb\tc\n1\t2\t3"; const tsvPath = await createTempMediaFile({ diff --git a/src/media-understanding/apply.ts b/src/media-understanding/apply.ts index 5639b17fa82..f7d5ecddbcf 100644 --- a/src/media-understanding/apply.ts +++ b/src/media-understanding/apply.ts @@ -382,7 +382,11 @@ async function extractFileBlocks(params: { } const utf16Charset = resolveUtf16Charset(bufferResult?.buffer); const textSample = decodeTextSample(bufferResult?.buffer); - const textLike = Boolean(utf16Charset) || looksLikeUtf8Text(bufferResult?.buffer); + // Do not coerce real PDFs into text/plain via printable-byte heuristics. + // PDFs have a dedicated extraction path in extractFileContentFromSource. + const allowTextHeuristic = normalizedRawMime !== "application/pdf"; + const textLike = + allowTextHeuristic && (Boolean(utf16Charset) || looksLikeUtf8Text(bufferResult?.buffer)); const guessedDelimited = textLike ? guessDelimitedMime(textSample) : undefined; const textHint = forcedTextMimeResolved ?? guessedDelimited ?? (textLike ? "text/plain" : undefined);