Stored XSS in Attachment Download Link via Dangerous MIME Types in PrivateBin
Summary
PrivateBin's attachment download link points to a same-origin blob: URL that carries an attacker-controlled Content-Type from a decrypted data URI. A DOMPurify sanitization pass added for CVE-2022-24833 only triggers for image/*svg* MIME types and only applies to an inline preview copy, never to the download link itself. An attacker uploads a text/html attachment, the sanitization regex doesn't match, and the "Download attachment" link's href stays an unsanitized blob:http://instance/... with Content-Type: text/html. When a victim right-clicks that link and selects "Open in new tab" (or middle-clicks), the browser renders it as a full HTML document in PrivateBin's origin, executing inline JavaScript with same-origin capability.
Requires fileupload = true (non-default) and a non-recommended CSP configuration. Instances using the default recommended CSP are protected because the blob inherits script-src 'self', which blocks inline scripts.
Technical Details: Root Cause
MIME-gated sanitization skips everything except SVG
AttachmentViewer.setAttachment in js/privatebin.js:2982 processes decrypted attachment data. Since PrivateBin uses zero-knowledge encryption, attachment content and MIME type are fully attacker-controlled and can't be inspected by the server.
At line 3001-3002, the download link's href is set to an unsanitized blob URL built from the raw decoded data and the attacker's MIME type:
// js/privatebin.js:3001-3002 let blobUrl = getBlobUrl(decodedData, mimeType); // unsanitized blob attachmentLink.attr('href', blobUrl); // download link set here, never updated later
The DOMPurify sanitization at line 3017-3023 only runs when the MIME type matches /^image/.*svg/i:
// js/privatebin.js:3017-3023 if (mimeType.match(/^image/.*svg/i)) { // only SVG is considered const sanitizedData = DOMPurify.sanitize(decodedData, purifySvgConfig); blobUrl = getBlobUrl(sanitizedData, mimeType); // reassigns LOCAL variable only }
A text/html attachment doesn't match this regex, so sanitization is skipped entirely. Even for SVG, the sanitized blob only feeds the inline preview at line 3028, while the download link at line 3002 retains the unsanitized original.
MIME type extraction from attacker-controlled data URI
getAttachmentMimeType at line 3211-3217 reads the substring between data: and ; in the decrypted data URI. Since this value comes from the attacker's encrypted payload, the attacker chooses whatever MIME type they want. getBlobUrl at line 2956-2971 then creates a Blob with that exact Content-Type, which becomes the download link's href.
Attack flow
- Attacker creates a paste with an attached .html file. The client encodes it as data:text/html;base64,... and encrypts it.
- Victim opens the paste URL. decryptPaste (line 5387-5397) decrypts the message and calls setAttachment with the attacker's data URI.
- setAttachment creates a same-origin blob:http://instance/... with Content-Type: text/html containing the attacker's HTML and script. This blob is assigned to the "Download attachment" link without sanitization.
- Victim right-clicks the link and selects "Open in new tab" (or middle-clicks). The browser renders the blob as HTML in the instance's origin, executing the attacker's inline JavaScript.
Relation to CVE-2022-24833
The 2022 advisory claimed that "whether you open the SVG in a new tab or not and whether CSP is present and enabled or not does not matter any more, as the displayed SVG is sanitized." This doesn't hold because the download link's blob is never sanitized (only the preview blob is), and the advisory's safety argument for the download link ("opens from file:// protocol") assumes the file is downloaded to disk. Opening the link in a new tab navigates to a same-origin blob: URL instead.