fix(security): derive the ACP image mimeType from decoded content - #4079
fix(security): derive the ACP image mimeType from decoded content#4079adiarora06 wants to merge 1 commit into
Conversation
f074b63 to
8db414c
Compare
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
8db414c to
70e53c8
Compare
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
2 similar comments
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
70e53c8 to
1af2810
Compare
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
4 similar comments
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
…rodotdev#3769) `acp/prompt_blocks.py` chose an image's wire `mimeType` from its path suffix. The suffix is a claim made by whoever named the file -- an upload handler that kept a client-supplied name, a channel that renamed an attachment, a screenshot tool with its own convention -- and every producer feeding this sink therefore needed its own guard to keep that claim honest. Two concrete failures came out of it: - A renamed image shipped mislabelled. An image already inside the dimension cap rides through `_downscale_within_limits` byte-identical, so a JPEG named `.png` reached the model as JPEG bytes declared `image/png`. - A non-image named like one was inlined anyway. `notes.png` became an image block the backend cannot decode. That is not a one-turn error: kiro-cli replays the full message history every turn, so a rejected block sits at a fixed history index and wedges the session from then on -- the same consequence the dimension and encoded-size caps already fail closed to avoid. The suffix now decides only which paths are CANDIDATES (it is what the regex matched on, and re-checking it keeps a `.txt` from costing a stat). What the file IS is decided from its bytes: - `_sniff_media_type` prefers Pillow's reported `format`, which comes from a real decode and is header-only, so it costs no decompression. - A magic-byte signature table is the no-Pillow fallback. A hand-stripped install would otherwise degrade to trusting the suffix, which is the property this sink exists to remove. `RIFF` is matched together with its form field so a WAV does not read as a WEBP. - An undecodable or unsupported candidate is left as a path, not inlined. - A content/suffix MISMATCH is not a refusal: the image is real, so it still travels, labelled by its content. Producers rename attachments routinely, and dropping a good image to placate a filename trades a capability for nothing. The mismatch is logged. This also fixes the re-encode, not just the label: `_downscale_within_limits` picks its Pillow save format from the mime, so a wrong mime produced a wrongly encoded payload as well as a wrong declaration. Scoped to the sink. The issue's follow-on -- removing now-redundant producer-side suffix tables -- is deliberately not here: those are defense in depth, and removing them is the part that needs the broader producer compatibility coverage kirodotdev#3754 flagged. - `test_acp_prompt_blocks.py`: new `TestMediaTypeFromContent` (14 tests) covers content-over-suffix, the renamed-but-real image, the fail-closed non-image / SVG-behind-a-raster-name / truncated cases, the signature table agreeing with Pillow on every supported format and refusing a RIFF that is not WEBP, and the re-encode following content. 16 tests fail on main. - `test_every_supported_suffix_maps_to_its_mime` now writes REAL bytes of each format; it previously wrote a PNG behind every suffix, which is why it could pass while the mime was read off the filename. - Full ACP/image suites: 2132 passed.
Head branch was pushed to by a user without write access
5d35b70 to
46c27e8
Compare
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
1 similar comment
|
👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated. Missing sections:
Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle. |
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed BLOCKING -- src/kiro_crew/acp/prompt_blocks.py:196 -- No-Pillow fallback accepts truncated image headers
|
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo blocking issues; one advisory finding. FINDING — src/kiro_crew/acp/prompt_blocks.py:130 — [OPUS-REVIEWED] 46c27e8 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound fail-closed direction, but the sniff duplicates format identity Watch
Suggestions
[DESIGN-REVIEWED] 46c27e8 |
First Principles Review (Fable 5, fork) — 🔴 BLOCKPremise-level review of First-Principles-Verdict: BLOCK The sink already detects content — What this change shipsIntent: stop a misnamed or non-image file from shipping a wrong
Blockers
[FIRST-PRINCIPLES-REVIEWED] 46c27e8 |
Audit note — part of this has already landed; the rest has notThis PR is not a duplicate and is not finished by anything on Which parts
|
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
|
Kiro Crew [operator: chenmingwei23]: This PR has been inactive for 7+ days. I reviewed the blockers but they require your input:
When you've addressed these, the pipeline will re-assess on its next cycle. |
|
@adiarora06 Thanks for this. Most of the headline claim has since landed on Already on Still missing on Could you rebase onto current On overlap with open #8238 (@snandi1603): both touch Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
Closes #3769.
Problem / Motivation
acp/prompt_blocks.pychose an image's wiremimeTypefrom its path suffix:A suffix is a claim made by whoever named the file — an upload handler that kept a client-supplied name, a channel that renamed an attachment, a screenshot tool with its own convention. Every producer feeding this shared sink therefore needed its own guard to keep that claim honest, which is what #3754's review flagged.
Two concrete failures follow:
_downscale_within_limitsbyte-identical (return raw_bytes, mime), so a JPEG named.pngreaches the model as JPEG bytes declaredimage/png.notes.pngbecomes an image block the backend cannot decode.Why it matters
The second failure above is not a one-turn error. kiro-cli replays the full message history every turn, so a rejected block sits at a fixed history index and wedges the session from then on — the same consequence
MAX_IMAGE_EDGE_PXandMAX_IMAGE_B64_BYTESalready fail closed to avoid. This path was failing open on the one question they do not ask, so a single mislabelled or misnamed file can permanently break a conversation rather than just failing that one turn.What changed
The suffix now decides only which paths are candidates — it is what
_PATH_REmatched on, and re-checking it keeps a.txtfrom costing astat. What the file is gets decided from its bytes, in a new_sniff_media_type:formatwins when Pillow is installed: it comes from a real decode, and it is header-only (Image.openpopulatesformatwithout decompressing), so this costs nothing on the common path.RIFFis matched together with its form field at offset 8, so a WAV does not read as a WEBP.This also fixes the re-encode, not just the label:
_downscale_within_limitspicks its Pillow save format from the mime (_PIL_SAVE_FORMAT), so a wrong mime produced a wrongly-encoded payload as well as a wrong declaration.Scope: Sink only. The issue's follow-on — "remove redundant producer-side image-suffix tables/renames where safe" — is deliberately not here. Those producer guards are defense in depth, and removing them is precisely the part that needs the broader producer-compatibility coverage #3754 declined to take on. Making the sink authoritative is the prerequisite; deleting the now-redundant guards can follow once this has soaked.
Tests
TestMediaTypeFromContent(14 tests): content wins over a misleading suffix; a renamed-but-real image is still inlined; fail-closed for a text file named.png, for SVG bytes behind a raster name (SVG is excluded by policy, and excluding it by suffix alone let the same bytes through under another name), and for a truncated image; the signature table agrees with Pillow on every supported format with_HAS_PILpatched off, and refuses aRIFFthat is not WEBP; the re-encode follows content; GIF still collapses to a PNG still frame.test_every_supported_suffix_maps_to_its_mimewas rewritten to write REAL bytes of each format. It previously wrote a PNG behind every suffix — which is exactly why it could pass while the mime was being read off the filename. That test pinned the bug.mainwith the source change reverted and the tests kept.test_acp_prompt_blocks.py: 86 passed, 1 skipped. Full ACP/image/upload/attachment suites: 2132 passed (5 unrelated failures are worktree-environment artifacts — anAF_UNIX path too long, two bare-interpreter import checks, and an RSS measurement flake).flake8/isortclean;./scripts/docs-lint.shpasses.docs/system-specs/modules/acp-client.mddocuments the content-derived contract and the fallback.