Skip to content

Rust: Axum State captured by a closure is treated as remote #22912

Closed

Description

@noctowlx

Description of the false positive

rust/path-injection reports a configured path from State<Arc<Config>> when the handler destructures State in its parameter and captures the binding in a spawn_blocking closure. The state is initialized with a constant during router construction; it is not request-derived. Reading state.0 into a local Arc clone before the same closure produces no finding. A request-derived Json value captured by the same API correctly produces a finding.

This is a remaining case after github/codeql#22466 and the State field-taint exclusion. That exclusion is already present in the tested library; adding it again would not address this reproduction.

Versions

  • CodeQL CLI 2.27.3; codeql/rust-queries 0.1.45; codeql/rust-all 0.2.24.
  • Rust pack build: a1575b98a8cc19bcfa7bb6cb41be688cb3dae65c.
  • Axum 0.8.4; Tokio 1.54.1, with feature rt.
  • Stock rust/path-injection query; default remote threat model; no custom source, barrier, or suppression model.

Code sample

use axum::{extract::State, routing::{get, post}, Json, Router};
use std::sync::Arc;

struct Config {
    path: String,
}

async fn destructured(State(config): State<Arc<Config>>) {
    let _ = tokio::task::spawn_blocking(move || std::fs::read_to_string(&config.path)).await;
}

async fn local_clone(state: State<Arc<Config>>) {
    let config = Arc::clone(&state.0);
    let _ = tokio::task::spawn_blocking(move || std::fs::read_to_string(&config.path)).await;
}

async fn request(Json(path): Json<String>) {
    let _ = tokio::task::spawn_blocking(move || std::fs::read_to_string(path)).await;
}

fn main() {
    let _: Router = Router::new()
        .route("/destructured", get(destructured))
        .route("/local-clone", get(local_clone))
        .route("/request", post(request))
        .with_state(Arc::new(Config { path: String::from("configured-file") }));
}

Expected and observed

Handler Expected findings Observed findings
destructured 0 1 false positive
local_clone 0 0
request 1 1

The false-positive path is destructured registration → State parameter → closure capture of config → config.path → std::fs::read_to_string. Only the Json handler should be a remote-to-file finding.

Reproduction with the upstream QL test runner

Place the sample in test.rs in a new test directory below rust/ql/test in a checkout of the codeql-cli/v2.27.3 tag, and add:

# options.yml
qltest_cargo_check: true
qltest_dependencies:
  - axum = { version = "=0.8.4" }
  - tokio = { version = "=1.54.1", features = ["rt"] }
# TaintedPath.qlref
query: queries/security/CWE-022/TaintedPath.ql

Run codeql test run <test-directory> with the listed packs. The sample compiles and the stock query returns the two findings shown above. A regression expecting only the Json finding fails with the additional State-to-file flow. No scanner or query was disabled.

Activity

  1. self-assigned this
    on Oct 28, 2026
  2. hvitved commented on Oct 28, 2026

    @hvitved
    Contributor

    Thank you for reporting this. Here is a fix: #22917.

  3. hvitved commented on Oct 30, 2026

    @hvitved
    Contributor

    Fix has been merged, so closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions