{"id":"CVE-2026-22257","aliases":["GHSA-54m3-5fxr-2f3j"],"url":"https://o3.security/vulnerability/CVE-2026-22257","summary":"Salvo is vulnerable to stored XSS in the list_html function by uploading files with malicious names","details":"# Summary\n\nThe function `list_html` generates a file view of a folder without sanitizing the files or folders names, potentially leading to XSS in cases where a website allows access to public files using this feature, allowing  anyone to upload a file.\n\n# Details\n\nThe vulnerable snippet of code is the following:\n[**dir.rs**](https://github.com/salvo-rs/salvo/blob/16efeba312a274739606ce76366d921768628654/crates/serve-static/src/dir.rs#L581)\n\n```rust\n// ... fn list_html(...\n        let mut link = \"\".to_owned();\n        format!(\n            r#\"<a href=\"/\">{}</a>{}\"#,\n            HOME_ICON,\n            segments\n                .map(|seg| {\n                    link = format!(\"{link}/{seg}\");\n                    format!(\"/<a href=\\\"{link}\\\">{seg}</a>\")\n                })\n                .collect::<Vec<_>>()\n                .join(\"\")\n        )\n// ...\n```\n\n# PoC\n\nhttps://github.com/user-attachments/assets/1e161e17-f033-4cc4-855b-43fd38ed1be4\n\nHere is the example app we used:\n\n`mian.rs`\n```rs\nuse salvo::prelude::*;\nuse salvo::serve_static::StaticDir;\nuse std::path::PathBuf;\nuse tokio::fs;\n\nconst INDEX_HTML: &str = r#\"<!doctype html>\n<html>\n  <head><meta charset=\"utf-8\"><title>StaticDir PoC</title></head>\n  <body>\n    <h2>Upload a file</h2>\n    <form action=\"/upload\" method=\"post\" enctype=\"multipart/form-data\">\n      <input type=\"file\" name=\"file\" />\n      <button type=\"submit\">Upload</button>\n    </form>\n\n    <p>Browse uploads:</p>\n    <ul>\n      <li><a href=\"/files\">/files</a></li>\n      <li><a href=\"/files/\">/files/</a></li>\n    </ul>\n  </body>\n</html>\n\"#;\n\n#[handler]\nasync fn index(res: &mut Response) {\n    res.render(Text::Html(INDEX_HTML));\n}\n\n#[handler]\nasync fn upload(req: &mut Request, res: &mut Response) {\n    fs::create_dir_all(\"uploads\").await.expect(\"create uploads dir\");\n\n    let form = match req.form_data().await {\n        Ok(v) => v,\n        Err(e) => {\n            res.status_code(StatusCode::BAD_REQUEST);\n            res.render(Text::Plain(format!(\"form_data parse failed: {e}\")));\n            return;\n        }\n    };\n\n    let Some(file_part) = form.files.get(\"file\") else {\n        res.status_code(StatusCode::BAD_REQUEST);\n        res.render(Text::Plain(\"missing file field (name=\\\"file\\\")\"));\n        return;\n    };\n\n    let original_name = file_part.name().unwrap_or(\"upload.bin\");\n\n    let mut dest = PathBuf::from(\"uploads\");\n    dest.push(original_name);\n\n    let tmp_path = file_part.path();\n    if let Err(e) = fs::copy(tmp_path, &dest).await {\n        res.status_code(StatusCode::INTERNAL_SERVER_ERROR);\n        res.render(Text::Plain(format!(\"save failed: {e}\")));\n        return;\n    }\n\n    res.render(Text::Plain(format!(\n        \"Uploaded as: {original_name}\\nNow open: http://127.0.0.1:5800/files/\\n\"\n    )));\n}\n\n#[tokio::main]\nasync fn main() {\n    tracing_subscriber::fmt().init();\n    fs::create_dir_all(\"uploads\").await.expect(\"create uploads dir\");\n\n    let router = Router::new()\n        .get(index)\n        .push(Router::with_path(\"upload\").post(upload))\n        .push(\n            Router::with_path(\"files/{**rest_path}\")\n                .get(StaticDir::new(\"uploads\").auto_list(true)),\n        );\n\n    let acceptor = TcpListener::new(\"127.0.0.1:5800\").bind().await;\n    Server::new(acceptor).serve(router).await;\n}\n```\n`Cargo.toml`\n```rs\n[package]\nname = \"poc\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nsalvo = { version = \"0.85.0\", features = [\"serve-static\"] }\ntokio = { version = \"1\", features = [\"macros\", \"rt-multi-thread\", \"fs\"] }\ntracing-subscriber = \"0.3\"\n```\n# Impact\n\nJavaScript execution, most likely leading to an account takeover, depending on the site's constraint (CSP, etc…).","published":"2026-01-08T18:22:05.661Z","modified":"2026-08-12T03:51:37.858459812Z","cvss":{"score":8.8,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"crates.io","name":"salvo","fixedVersion":"0.88.1"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/salvo-rs/salvo/blob/16efeba312a274739606ce76366d921768628654/crates/serve-static/src/dir.rs#L581"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/22xxx/CVE-2026-22257.json"},{"type":"ADVISORY","url":"https://github.com/salvo-rs/salvo/security/advisories/GHSA-54m3-5fxr-2f3j"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-22257"},{"type":"PACKAGE","url":"https://github.com/salvo-rs/salvo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:37.858459812Z"}}