Skip to content

CON-47: check the scheme of the url a Blossom server returns - #92

Merged
molgerz merged 2 commits into
mainfrom
con-47-blossom-attachment-url
Sep 19, 2026
Merged

molgerz merged 2 commits into
mainfrom
con-47-blossom-attachment-url

Conversation

@molgerz

@molgerz molgerz commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes CON-47. From the CON-43 security audit.

The Blossom server answers an upload with a descriptor, and its url was taken at its word and written straight into page content.

What is in it

  • resolveAttachmentUrl accepts the returned url only once it parses as an absolute http(s) URL. javascript:alert(1) and data:text/html,… are perfectly valid URLs, so it is the protocol check and not the parse that rejects them; anything else falls back to the deterministic <server>/<sha256>, which is where the blob has to be anyway.
  • The string handed on is the exact one the parser validated. new URL ignores leading and trailing C0 controls and spaces, so passing the unstripped string on would have stored %20https://x/a%20 — a destination that decodes to a relative path with a space in it. Not trim(), which would also strip a trailing U+00A0 that is part of the blob's name.
  • The destination is percent-encoded where Markdown would end the link early: a bare ) closes it, whitespace starts the optional title, </> are the other destination form, a backslash escapes what follows. Encoded over UTF-8 bytes — \s matches non-ASCII whitespace too, and encoding U+2003 from its code unit yields %2003, i.e. %20 followed by a literal 03.
  • Our own renderer already refuses those schemes and the server is operator-configured: this is the second line, so that what is stored in a page is sound for every future reader and not only for this client.

How to test

  1. Upload an image and a file through the editor's insert menu: both still embed and open correctly.
  2. Point VITE_BLOSSOM_SERVER at a stub that answers with {"url":"javascript:alert(1)"} — the inserted link is <server>/<hash>, not the returned string. Same for a data: url and for a relative one.
  3. A stub answering with a url padded by a space inserts the trimmed url, and the link resolves.
  4. A stub answering with a url containing ) or a space in the path inserts a percent-encoded destination, and the link still opens.
  5. VITE_BLOSSOM_SERVER ending in / produces no // in the stored url.
  6. src/nostr/blossom.test.ts covers the table.

…ape it

The server answers an upload with a descriptor whose `url` was taken at its
word and written straight into page content. `javascript:` and `data:` parse as
perfectly valid URLs, so it is the protocol check and not the parse that
rejects them. Our own renderer refuses those schemes and the server is
operator-configured, so this is the second line — what is *stored* in a page
should be sound for every future reader, not only for this client.

The string handed on is the exact one the parser validated: `new URL` ignores
leading and trailing C0 controls and spaces, so passing the unstripped string
on would store `%20https://x/a%20` — a destination that decodes to a relative
path with a space in it, a broken link built out of a URL that was fine. Not
`trim()`, which would also strip a trailing U+00A0 that is part of the blob's
name.

The destination is percent-encoded where Markdown would end the link early: a
bare `)` closes it, whitespace starts the optional title, `<`/`>` are the other
destination form and a backslash escapes what follows. Encoded over UTF-8
bytes, because `\s` matches non-ASCII whitespace too and encoding U+2003 from
its code unit yields `%2003` — `%20` followed by a literal `03`, pointing the
link somewhere else entirely. The label was already guarded from the other
side; both halves now happen at insertion time.
…ment-url

# Conflicts:
#	docs/09-security-privacy.md
@molgerz
molgerz merged commit 745be78 into main Sep 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants