Skip to content

id(web/markdown): image src bypasses the #5745 scheme neutralization — javascript:/data:text/html/vbscript:/file: survive import, raw-HTML <img>, collab PM JSON, and save #6364

Open

Description

@developing-today

Summary

The #5745 fix (PR #6339, merged today) neutralizes executable URL schemes in Markdown links at every entry point of pkgs/id/src/web/markdown.rs — but image src was left out of the same treatment in every direction. An image whose source is javascript:, data:text/html, vbscript:, file:, or any other non-image scheme survives import, raw-HTML <img> import, the raw-ProseMirror-JSON (collab) path, and save — sized images even emit it verbatim into a raw <img src="…"> HTML tag. Links in the same document, through the same converter, are neutralized to #.

This is the exact gap #5745's own oracle says not to leave: link_url_is_safe (markdown.rs:3943) documents that safety must come from an explicit allowed-scheme set and "do not rely on browser sanitization". Image sources currently rely on browser behaviour alone.

Evidence (file:line, main @ 6f4e4e3)

Sanitized (links, for contrast):

  • markdown.rs:2992 — parse direction: "href": sanitize_link_url(&link.url)
  • markdown.rs:3943 / :3984 — link_url_is_safe / sanitize_link_url (allowed: http, https, mailto, or scheme-less)
  • markdown.rs:4016, :5047, :5271, :5616 — raw-HTML anchor href and all save-direction link hrefs also sanitized

Not sanitized (images):

  • markdown.rs:3013-3015 — parse direction, NodeValue::Image arm: "src": image.url verbatim
  • markdown.rs:3903 — parse_img_tag: raw-HTML <img src> imported verbatim (let src = attr("src")?)
  • markdown.rs:5060 — save direction, unsized image: url: src.to_owned() verbatim
  • markdown.rs:5023 — save direction, sized image: format!("<img src=\"{}\"", escape_attr_value(src)) — attribute-escaped only; the scheme itself is emitted verbatim into an HTML tag
  • pkgs/id/web/src/image-node-view.ts:55 — the editor assigns the stored value directly: img.src = node.attrs.src || ""

Verified this run (laptop, real id crate, probe ~/git/fleet/probe-md-img-scheme, main @ 6f4e4e3)

Probe called the public id::web::{markdown_to_prosemirror, prosemirror_to_markdown} (feature web):

Parse direction (markdown_to_prosemirror):

  • ![x](javascript:alert(1)) → image src = "javascript:alert(1)"
  • ![x](JaVaScRiPt:alert(1)) → src = "JaVaScRiPt:alert(1)"
  • ![x](data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==) → src preserved verbatim
  • ![x](vbscript:msgbox(1)) → preserved; ![x](file:///etc/passwd) → preserved
  • <img src="javascript:alert(1)" alt="x"> → image src preserved verbatim
  • <img src="data:text/html;base64,PHNjcmlwdD4=" alt="x" width="10"> → src + width preserved verbatim
  • Control, same converter: [x](javascript:alert(1)) → link mark href = "#" (the id(web): neutralize executable URL schemes in imported Markdown links #5745 behaviour)

Save direction (prosemirror_to_markdown on raw ProseMirror JSON — the collab Rich path that never passed the parse-direction converter):

  • image src = "javascript:alert(1)" → saved as ![x](javascript:alert(1))
  • image src = "data:text/html;base64,PHNjcmlwdD4=" → saved verbatim
  • sized image (width 10) src = "javascript:alert(1)" → saved as <img src="javascript:alert(1)" alt="x" width="10" />

Controls that must keep working: https://example.com/a.png, /local/a.png, and data:image/png;base64,iVBORw0KGgo= all parse with src preserved (see the policy note — the last one is why the image oracle cannot simply reuse the link oracle).

Why it matters

  • Stored/collaborative documents are shared content: a doc imported from Markdown, pasted as raw HTML, or pushed as raw ProseMirror JSON over collab can plant an image node whose src is an executable or local-file scheme, and it round-trips indefinitely — every save re-emits it, including as a raw HTML <img> tag for sized images. Any present or future consumer that renders that HTML, opens the image in a top-level context, or follows the src outside an <img> element inherits the payload. id(web): neutralize executable URL schemes in imported Markdown links #5745 was filed and fixed at priority/high precisely because this converter must not rely on downstream/browser sanitization.
  • The asymmetry is invisible: a reader of the id(web): neutralize executable URL schemes in imported Markdown links #5745 fix reasonably believes "URL schemes in this codec are neutralized", while the image half of the same codec is untouched.

Policy note (the design call for triage)

The image oracle is not the link oracle verbatim: data:image/* (at least the raster types, e.g. data:image/png) is a legitimate, currently-working image source and must not be neutralized. The fix needs an image-specific allowed set — e.g. scheme-less / http / https / data:image/<raster> — with everything else (javascript:, vbscript:, data:text/html, data:image/svg+xml if script-bearing SVG is in scope, file:, unknown schemes, and TAB/LF/CR-obfuscated spellings of them, mirroring link_url_is_safe's stripping probe) neutralized the same way links are. Whether to neutralize to "#" (link precedent) or drop/replace the src is part of the same call.

Acceptance criteria

  • An image-specific scheme check exists beside link_url_is_safe in markdown.rs, with the allowed set documented as the oracle (including the data:image/* decision and its rationale)
  • Parse direction: Markdown images and raw-HTML <img> imports with a disallowed scheme no longer produce a PM image node carrying that src (neutralized per the chosen shape); TAB/LF/CR-obfuscated and mixed-case spellings are caught
  • Save direction: raw ProseMirror JSON image nodes with a disallowed src are neutralized in both the unsized (![alt](…)) and sized (<img …>) emission paths, and a sanitized document is a stable round-trip fixpoint (re-parsing/re-saving changes nothing further)
  • Controls preserved byte-for-byte in both directions: http:/https: images, relative/absolute local paths, and the allowed data:image/* forms
  • Pinning tests in the markdown module cover each path above, in the style of the id(web): neutralize executable URL schemes in imported Markdown links #5745 tests (test_link_executable_schemes_neutralized and siblings), naming the image oracle
  • The linked-image case still works: the wrapping link's linkHref keeps its id(web): neutralize executable URL schemes in imported Markdown links #5745 neutralization and the image src check does not break [![alt](src)](href) round-trips

Dedup

gh search issues for "5745 image", "markdown image src scheme", "sanitize image", "img src" (2026-11-05): #5745 (closed) is links-only in title, body, and fix; #873 (closed) is attribute escaping of src/alt/title, not schemes; no open or closed issue covers image-src schemes. Related: #5745 / PR #6339 (the link fix this extends).

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 5, 2026
  2. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    🤖 Agent note · filed

  3. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    🤖 Agent note · handed off

  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 5, 2026
  5. developing-today commented on Nov 5, 2026

    @developing-today
    ContributorAuthor

    agent-note:v1
    state: triaged (confirmed; fix task enqueued)
    queue: repo-health-code-pilot
    task: 10723 (triage; worker repo-worker-code; lane code-z)
    by: repo-queue-worker-shift-z
    at: 2026-11-05T19:12Z

    CONFIRMED fresh against laptop main @aab9b8c1eb (gh + laptop grep, 2026-11-05T19:09Z): issue #6364 OPEN, no hard-stop labels. In pkgs/id/src/web/markdown.rs the link sanitizer is present (link_url_is_safe :3943, sanitize_link_url :3984, applied :2992/:4016/:5047/:5271/:5616) while all four image sites remain verbatim — :3015 parse-direction "src": image.url, :3903 parse_img_tag attr("src"), :5023 sized save escaped-only, :5060 unsized save url verbatim — and there is no data:image handling anywhere in the file. The creator's probe evidence (@6f4e4e37a9) therefore still describes current main exactly.

    TRIAGE ORACLE DECISION (the policy call this triage owned): the image oracle is image-specific, not the link oracle — allowed: scheme-less, http, https, and data:image/ for raster subtypes png/gif/jpeg/webp/avif/bmp only. data:image/svg+xml is NOT allowed (script-bearing). Every other scheme (javascript:, vbscript:, data:text/html, file:, unknown) plus TAB/LF/CR-obfuscated and mixed-case spellings is neutralized to "#" mirroring sanitize_link_url, so sanitized docs are a stable round-trip fixpoint.

    Route: fix task 10725 enqueued (small_fix, dedupe repo-health:v1:fix:developing-today/code:issue:6364) with the oracle, the four sites, and pinning tests in the #5745 style. No code was changed in this triage; tests are the fix task's acceptance.

  6. 19 remaining items

  7. dezren39 commented on Nov 5, 2026

    @dezren39
    Member

    🤖 Agent note · handed off

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 5, 2026
  9. dezren39 commented on Nov 5, 2026

    @dezren39
    Member

    🤖 Agent note · picked up

  10. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 5, 2026
  11. dezren39 commented on Nov 5, 2026

    @dezren39
    Member

    🤖 Agent note · handed off

  12. dezren39 commented on Nov 5, 2026

    @dezren39
    Member

    🤖 Agent note · handed off

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)queue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobstatus/parkedDeliberately paused with a recorded resume conditionstatus/triagedTriaged and routed (see agent note)task/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions