Get a Demo

Let's Patch It!

Book a short call with one our specialists, we'll walk you through how Endor Patches work, and ask you a few questions about your environment (like your primary programming languages and repository management). We'll also send you an email right after you fill out the form, feel free to reply with any questions you have in advance!

CVE

GHSA-54m3-5fxr-2f3j

Salvo is vulnerable to stored XSS in the list_html function by uploading files with malicious names
Back to all
CVE

GHSA-54m3-5fxr-2f3j

Salvo is vulnerable to stored XSS in the list_html function by uploading files with malicious names

Summary

The 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.

Details

The vulnerable snippet of code is the following:

dir.rs

// ... fn list_html(...
        let mut link = "".to_owned();
        format!(
            r#"<a href="/">{}</a>{}"#,
            HOME_ICON,
            segments
                .map(|seg| {
                    link = format!("{link}/{seg}");
                    format!("/<a href=\"{link}\">{seg}</a>")
                })
                .collect::<Vec<_>>()
                .join("")
        )
// ...

PoC

https://github.com/user-attachments/assets/1e161e17-f033-4cc4-855b-43fd38ed1be4

Here is the example app we used:

mian.rs

use salvo::prelude::*;
use salvo::serve_static::StaticDir;
use std::path::PathBuf;
use tokio::fs;
const INDEX_HTML: &str = r#"<!doctype html>
<html>
  <head><meta charset="utf-8"><title>StaticDir PoC</title></head>
  <body>
    <h2>Upload a file</h2>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="file" />
      <button type="submit">Upload</button>
    </form>
    <p>Browse uploads:</p>
    <ul>
      <li><a href="/files">/files</a></li>
      <li><a href="/files/">/files/</a></li>
    </ul>
  </body>
</html>
"#;
#[handler]
async fn index(res: &mut Response) {
    res.render(Text::Html(INDEX_HTML));
}
#[handler]
async fn upload(req: &mut Request, res: &mut Response) {
    fs::create_dir_all("uploads").await.expect("create uploads dir");
    let form = match req.form_data().await {
        Ok(v) => v,
        Err(e) => {
            res.status_code(StatusCode::BAD_REQUEST);
            res.render(Text::Plain(format!("form_data parse failed: {e}")));
            return;
        }
    };
    let Some(file_part) = form.files.get("file") else {
        res.status_code(StatusCode::BAD_REQUEST);
        res.render(Text::Plain("missing file field (name=\"file\")"));
        return;
    };
    let original_name = file_part.name().unwrap_or("upload.bin");
    let mut dest = PathBuf::from("uploads");
    dest.push(original_name);
    let tmp_path = file_part.path();
    if let Err(e) = fs::copy(tmp_path, &dest).await {
        res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
        res.render(Text::Plain(format!("save failed: {e}")));
        return;
    }
    res.render(Text::Plain(format!(
        "Uploaded as: {original_name}\nNow open: http://127.0.0.1:5800/files/\n"
    )));
}
#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();
    fs::create_dir_all("uploads").await.expect("create uploads dir");
    let router = Router::new()
        .get(index)
        .push(Router::with_path("upload").post(upload))
        .push(
            Router::with_path("files/{**rest_path}")
                .get(StaticDir::new("uploads").auto_list(true)),
        );
    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

Cargo.toml

[package]
name = "poc"
version = "0.1.0"
edition = "2024"
[dependencies]
salvo = { version = "0.85.0", features = ["serve-static"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] }
tracing-subscriber = "0.3"

Impact

JavaScript execution, most likely leading to an account takeover, depending on the site's constraint (CSP, etc…).

Package Versions Affected

Package Version
patch Availability
No items found.

Automatically patch vulnerabilities without upgrading

Fix Without Upgrading
Detect compatible fix
Apply safe remediation
Fix with a single pull request

CVSS Version

Severity
Base Score
CVSS Version
Score Vector
C
H
U
8.8
-
3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L
C
H
U
0
-
3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L
C
H
U
-

Related Resources

No items found.

References

https://github.com/salvo-rs/salvo/security/advisories/GHSA-54m3-5fxr-2f3j, https://nvd.nist.gov/vuln/detail/CVE-2026-22257, https://github.com/salvo-rs/salvo, https://github.com/salvo-rs/salvo/blob/16efeba312a274739606ce76366d921768628654/crates/serve-static/src/dir.rs#L581

Severity

8.8

CVSS Score
0
10

Basic Information

Ecosystem
Base CVSS
8.8
EPSS Probability
0.00013%
EPSS Percentile
0.01635%
Introduced Version
0
Fix Available
0.88.1

Fix Critical Vulnerabilities Instantly

Secure your app without upgrading.
Fix Without Upgrading