Summary
actix-files escapes the visible text of filenames in generated directory listings, but it percent-encodes the same attacker-controlled path for a double-quoted href attribute with percent_encoding::CONTROLS. CONTROLS does not encode double quotes, <, or >. On Unix-like filesystems, a filename containing those characters can terminate the href attribute and inject HTML with an event handler.
This regresses the special-character escaping added by PR #355. Commit 55179d6ab2c602617d5c1391728d041223ae8358 replaced DEFAULT_ENCODE_SET with the much narrower CONTROLS during the percent-encoding 2.x migration.
Details
Affected and tested
- actix-files 0.7.2 with actix-web 4.18.1
- current repository commit 5f4d20906a7e93074558005adc7e161db67fdab8
- Git history indicates actix-files >= 0.1.5 through 0.7.2 and current main, pending maintainer confirmation
Preconditions
The application opts in to Files::show_files_listing(); an attacker or attacker-controlled upstream can create a file or directory with an attacker-chosen name below the listed root; the filesystem permits double quotes and angle brackets (the PoC targets Linux/Unix); a victim browses the listing; and no deployment-wide CSP independently blocks inline event handlers. Listings are disabled by default.
Root cause
actix-files/src/directory.rs defines encode_file_url! as utf8_percent_encode(path, CONTROLS) and serializes it directly into <a href="{}">. The visible filename is separately HTML-escaped, but CONTROLS covers only ASCII control bytes and leaves the attribute and tag delimiters unencoded. The percent-encoding migration guidance required callers to construct a domain-appropriate set; replacing DEFAULT_ENCODE_SET with bare CONTROLS did not preserve the earlier behavior.
Actual generated HTML contains an href closed by the filename followed by an injected img onerror element, while the visible link text remains escaped. Headless Chrome confirmed execution by producing <body data-actix-xss="1">. The renderer assertions passed twice on stable versions and against current commit 5f4d20906a7e93074558005adc7e161db67fdab8.
History and duplicate review
PR #355 fixed special-character injection in the same renderer in 2018. PRs #584, #651, and #1953 changed visible-text escaping or refactored code but did not fix the current href regression. Issues #653 and #3207 concern spaces and path_filter visibility. Current public advisories cover request smuggling, an empty-Range panic, and missing-directory information exposure. Final searches across public/closed issues, PRs, commits, advisories, releases, GitHub Advisory Database, CVE/NVD, and exact identifiers such as encode_file_url, show_files_listing, CONTROLS, filename/href escaping, and stored directory-listing XSS found no exact current duplicate.
Similar advisories in other products, including static-web-server GHSA-rwfq-v4hq-h7fg / CVE-2024-32966 and Mongoose GHSA-5g6j-m3pv-4f7g, support the impact class but are not Actix duplicates.
Suggested remediation
Encode each path component with a strict RFC 3986 path-segment set or a reviewed custom AsciiSet that includes all HTML/URL delimiters needed by this sink, then join components with literal slashes. HTML-escape the final serialized href as defense in depth. Add Unix-only regression cases for double quote, <, >, &, #, ?, and backtick and assert that no extra element or attribute is created.
Suggested classification: CWE-79. Conservative CVSS 3.1: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N (5.4, Moderate). Maintainers should decide final severity, range, GHSA/CVE handling, and whether this is a regression of PR #355.
This report was prepared with OpenAI Codex assistance. I directly reviewed the policy, source, history, duplicate search, PoC, repeated output, and browser execution evidence. If confirmed, please credit GitHub user kigma00.
Proof of concept
Prepared archive: actix-files-directory-listing-xss-poc.zip
SHA-256: 278136337517053ABA3BE6037ACC2FDC5D2B6E291D547CBFCEE3C84C518A81B6
The archive contains only Cargo.toml, Cargo.lock, src/main.rs, README.md, and browser-dom.txt. No dependencies, binaries, credentials, or third-party data are included. GitHub's structured form has not yet received the ZIP; the exact archive is available privately through a maintainer-approved channel.
Environment
- Windows 11 host with WSL Linux filesystem semantics
- rustc 1.99.1
- cargo 1.99.1
- actix-web 4.18.1
- actix-files 0.7.2
Run:
unzip actix-files-directory-listing-xss-poc.zip
cd actix-files-directory-listing-xss-poc
cargo run --locked
The PoC binds no network socket and uses actix_web::test with Files::new("/", root).show_files_listing(). It creates safe.txt and this malicious filename:
x"><img src=x onerror="document.body.setAttribute('data-actix-xss','1')">.txt
Repeated output:
actix_web_version=4.18.1
actix_files_version=0.7.2
safe_link_present=true
injected_element_present=true
visible_name_escaped=true
Expected: safe.txt remains a normal link and the malicious filename cannot terminate href or create an element.
Actual: the raw quote closes href and the browser creates an injected img element. Headless Chrome returned exit code 0 and produced <body data-actix-xss="1">, confirming execution. The same renderer output and assertions passed against current commit 5f4d20906a7e93074558005adc7e161db67fdab8.
Impact
When the stated preconditions hold, an attacker who controls a filename can persist JavaScript in an actix-files directory listing. A victim opening the listing executes attacker-controlled script in the serving origin. This may expose same-origin data or permit authenticated same-origin actions. HttpOnly, SameSite, CSRF defenses, and strict CSP can reduce particular consequences but do not correct the output-encoding flaw.