Skip to content

Rust: Add tests for web frameworks as taint sources #19466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@
| test.rs:369:25:369:43 | ...::open | Flow source 'FileSource' of type file (DEFAULT). |
| test.rs:377:22:377:35 | ...::stdin | Flow source 'StdInSource' of type stdin (DEFAULT). |
| test.rs:386:16:386:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). |
| web_frameworks.rs:12:31:12:31 | a | Flow source 'RemoteSource' of type remote (DEFAULT). |
| web_frameworks.rs:21:31:21:36 | TuplePat | Flow source 'RemoteSource' of type remote (DEFAULT). |
| web_frameworks.rs:43:31:43:45 | MyStruct {...} | Flow source 'RemoteSource' of type remote (DEFAULT). |
| web_frameworks.rs:51:31:51:32 | ms | Flow source 'RemoteSource' of type remote (DEFAULT). |
| web_frameworks.rs:60:15:60:15 | a | Flow source 'RemoteSource' of type remote (DEFAULT). |
5 changes: 5 additions & 0 deletions rust/ql/test/library-tests/dataflow/sources/options.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ qltest_dependencies:
- http = { version = "1.2.0" }
- tokio = { version = "1.43.0", features = ["full"] }
- futures = { version = "0.3" }
- poem = { version = "3.1.10" }
- serde = { version = "1.0.219" }
- actix-web = { version = "4.10.2" }
- axum = { version = "0.8.4" }
- serde_json = { version = "1.0.140" }
198 changes: 198 additions & 0 deletions rust/ql/test/library-tests/dataflow/sources/web_frameworks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@

fn sink<T>(_: T) { }

// --- tests ---

mod poem_test {
use poem::{get, handler, web::Path, web::Query, Route, Server, listener::TcpListener};
use serde::Deserialize;
use super::sink;

#[handler]
fn my_poem_handler_1(Path(a): Path<String>) -> String { // $ Alert[rust/summary/taint-sources]
sink(a.as_str()); // $ MISSING: hasTaintFlow
sink(a.as_bytes()); // $ MISSING: hasTaintFlow
sink(a); // $ MISSING: hasTaintFlow

"".to_string()
}

#[handler]
fn my_poem_handler_2(Path((a, b)): Path<(String, String)>) -> String { // $ Alert[rust/summary/taint-sources]
sink(a); // $ MISSING: hasTaintFlow
sink(b); // $ MISSING: hasTaintFlow

"".to_string()
}

#[handler]
fn my_poem_handler_3(path: Path<(String, String)>) -> String { // $ MISSING: Alert[rust/summary/taint-sources]
sink(&path.0); // $ MISSING: hasTaintFlow
sink(&path.1); // $ MISSING: hasTaintFlow

"".to_string()
}

#[derive(Deserialize)]
struct MyStruct {
a: String,
b: String,
}

#[handler]
fn my_poem_handler_4(Path(MyStruct {a, b}): Path<MyStruct>) -> String { // $ Alert[rust/summary/taint-sources]
sink(a); // $ MISSING: hasTaintFlow
sink(b); // $ MISSING: hasTaintFlow

"".to_string()
}

#[handler]
fn my_poem_handler_5(Path(ms): Path<MyStruct>) -> String { // $ Alert[rust/summary/taint-sources]
sink(ms.a); // $ MISSING: hasTaintFlow
sink(ms.b); // $ MISSING: hasTaintFlow

"".to_string()
}

#[handler]
fn my_poem_handler_6(
Query(a): Query<String>, // $ Alert[rust/summary/taint-sources]
) -> String {
sink(a); // $ MISSING: hasTaintFlow

"".to_string()
}

async fn test_poem() {
let app = Route::new()
.at("/1/:a", get(my_poem_handler_1))
.at("/2/:a/:b", get(my_poem_handler_2))
.at("/3/:a/:b", get(my_poem_handler_3))
.at("/4/:a/:b", get(my_poem_handler_4))
.at("/5/:a/:b", get(my_poem_handler_5))
.at("/6/:a/", get(my_poem_handler_6));

Server::new(TcpListener::bind("0.0.0.0:3000")).run(app).await.unwrap();

// ...
}
}

mod actix_test {
use actix_web::{get, web, App};
use super::sink;

async fn my_actix_handler_1(path: web::Path<String>) -> String { // $ MISSING: Alert[rust/summary/taint-sources]
let a = path.into_inner();
sink(a.as_str()); // $ MISSING: hasTaintFlow
sink(a.as_bytes()); // $ MISSING: hasTaintFlow
sink(a); // $ MISSING: hasTaintFlow

"".to_string()
}

async fn my_actix_handler_2(path: web::Path<(String, String)>) -> String { // $ MISSING: Alert[rust/summary/taint-sources]
let (a, b) = path.into_inner();

sink(a); // $ MISSING: hasTaintFlow
sink(b); // $ MISSING: hasTaintFlow

"".to_string()
}

async fn my_actix_handler_3(web::Query(a): web::Query<String>) -> String { // $ MISSING: Alert[rust/summary/taint-sources]
sink(a); // $ MISSING: hasTaintFlow

"".to_string()
}

#[get("/4/{a}")]
async fn my_actix_handler_4(path: web::Path<String>) -> String { // $ MISSING: Alert[rust/summary/taint-sources]
let a = path.into_inner();
sink(a); // $ MISSING: hasTaintFlow

"".to_string()
}

async fn test_actix() {
let app = App::new()
.route("/1/{a}", web::get().to(my_actix_handler_1))
.route("/2/{a}/{b}", web::get().to(my_actix_handler_2))
.route("/3/{a}", web::get().to(my_actix_handler_3))
.service(my_actix_handler_4);

// ...
}
}

mod axum_test {
use axum::Router;
use axum::routing::get;
use axum::extract::{Path, Query, Request, Json};
use std::collections::HashMap;
use super::sink;

async fn my_axum_handler_1(Path(a): Path<String>) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
sink(a.as_str()); // $ MISSING: hasTaintFlow
sink(a.as_bytes()); // $ MISSING: hasTaintFlow
sink(a); // $ MISSING: hasTaintFlow

""
}

async fn my_axum_handler_2(Path((a, b)): Path<(String, String)>) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
sink(a); // $ MISSING: hasTaintFlow
sink(b); // $ MISSING: hasTaintFlow

""
}

async fn my_axum_handler_3(Query(params): Query<HashMap<String, String>>) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
for (key, value) in params {
sink(key); // $ MISSING: hasTaintFlow
sink(value); // $ MISSING: hasTaintFlow
}

""
}

async fn my_axum_handler_4(request: Request) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
sink(request.body()); // $ MISSING: hasTaintFlow
request.headers().get("header").unwrap(); // $ MISSING: hasTaintFlow
sink(request.into_body()); // $ MISSING: hasTaintFlow

""
}

async fn my_axum_handler_5(Json(payload): Json<serde_json::Value>) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
sink(payload.as_str()); // $ MISSING: hasTaintFlow
sink(payload); // $ MISSING: hasTaintFlow

""
}

async fn my_axum_handler_6(body: String) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
sink(body); // $ MISSING: hasTaintFlow

""
}

async fn my_axum_handler_7(body: String) -> &'static str { // $ MISSING: Alert[rust/summary/taint-sources]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these Axum handlers that are just functions without any attributes I guess we'll have to find the get call where they are used? Could it make sense to have the path start at the get call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have the choice to match either the #[] attributes, the function arguments / types, or the way they're used in router setup (the get call). We also have the choice of making three library specific models or one heuristic model (I'd prefer the latter, as there are probably more than three web libraries for Rust, but lets wait and see what works best). In any case the goal here is just for the tests to be realistic enough to serve whichever approach to modelling we settle on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Though in this particular case it seems that we only have the get call to go by. Nothing else about the function stands out as a handler. Anyway, I was just curious, agree that we'll see later and that the test is good as-is :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think you might be right it'll be the trickiest one to model for that reason. Even if we don't succeed, the test serves to document that gap and potential for future improvement.

sink(body); // $ MISSING: hasTaintFlow

""
}

async fn test_axum() {
let app = Router::<()>::new()
.route("/1/{a}", get(my_axum_handler_1))
.route("/2/{a}/{b}", get(my_axum_handler_2))
.route("/3/:a", get(my_axum_handler_3))
.route("/4/:a", get(my_axum_handler_4))
.route("/5/:a", get(my_axum_handler_5))
.route("/67/:a", get(my_axum_handler_6).get(my_axum_handler_7));

// ...
}
}