PT-2026-6365 · Crates.Io · Salvo

Published

2026-01-08

·

Updated

2026-01-08

CVSS v3.1

8.8

High

VectorAV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:L

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
rust
// ... 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

Here is the example app we used:
mian.rs
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}
Now open: http://127.0.0.1:5800/files/
"
  )));
}

#[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
rs
[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…).

Fix

XSS

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-54M3-5FXR-2F3J

Affected Products

Salvo