PT-2026-6365 · Crates.Io · Salvo

Publicado

2026-01-08

·

Atualizado

2026-01-08

CVSS v3.1

8.8

Alta

VetorAV: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…).

Correção

XSS

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-54M3-5FXR-2F3J

Produtos afetados

Salvo