Skip to content
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 @@ -19,6 +19,7 @@ ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
ql/rust/ql/src/queries/security/CWE-918/RequestForgery.ql
ql/rust/ql/src/queries/summary/LinesOfCode.ql
ql/rust/ql/src/queries/summary/LinesOfUserCode.ql
ql/rust/ql/src/queries/summary/NodesWithTypeAtLengthLimit.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
ql/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
ql/rust/ql/src/queries/security/CWE-918/RequestForgery.ql
ql/rust/ql/src/queries/summary/LinesOfCode.ql
ql/rust/ql/src/queries/summary/LinesOfUserCode.ql
ql/rust/ql/src/queries/summary/NodesWithTypeAtLengthLimit.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
ql/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
ql/rust/ql/src/queries/security/CWE-918/RequestForgery.ql
ql/rust/ql/src/queries/summary/LinesOfCode.ql
ql/rust/ql/src/queries/summary/LinesOfUserCode.ql
ql/rust/ql/src/queries/summary/NodesWithTypeAtLengthLimit.ql
Expand Down
4 changes: 2 additions & 2 deletions rust/ql/lib/codeql/rust/frameworks/reqwest.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ extensions:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["<reqwest::async_impl::client::Client>::request", "Argument[1]", "transmission", "manual"]
- ["<reqwest::blocking::client::Client>::request", "Argument[1]", "transmission", "manual"]
- ["<reqwest::async_impl::client::Client>::request", "Argument[1]", "request-url", "manual"]
- ["<reqwest::blocking::client::Client>::request", "Argument[1]", "request-url", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ module CleartextTransmission {
* A sink defined through MaD.
*/
private class ModelsAsDataSink extends Sink {
ModelsAsDataSink() { sinkNode(this, "transmission") }
ModelsAsDataSink() { sinkNode(this, ["transmission", "request-url"]) }
Copy link
Contributor

Choose a reason for hiding this comment

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

These places are also sinks for the "plaintext transmission" query and we had such sinks labeled as transmission in MaD yaml. I'd like to reuse those for this query, so I've changed the label to request-url and this label is picked up by both queries.

This looks like a good design, both queries use request-url but we can still define a non-URL transmission only sink if we want to (I don't think we have any right now, but we surely will at some point).

}
}
49 changes: 49 additions & 0 deletions rust/ql/lib/codeql/rust/security/RequestForgeryExtensions.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Provides classes and predicates for reasoning about request forgery
* vulnerabilities.
*/

import rust
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.dataflow.FlowSink
private import codeql.rust.dataflow.FlowSource
private import codeql.rust.Concepts

/**
* Provides default sources, sinks and barriers for detecting request forgery
* vulnerabilities, as well as extension points for adding your own.
*/
module RequestForgery {
/**
* A data flow source for request forgery vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }

/**
* A data flow sink for request forgery vulnerabilities.
*/
abstract class Sink extends QuerySink::Range {
/**
* Gets the name of a part of the request that may be tainted by this sink,
* such as the URL or the host.
*/
override string getSinkType() { result = "RequestForgery" }
}

/**
* A barrier for request forgery vulnerabilities.
*/
abstract class Barrier extends DataFlow::Node { }

/**
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }

/**
* A sink for request forgery from model data.
*/
private class ModelsAsDataSink extends Sink {
ModelsAsDataSink() { sinkNode(this, "request-url") }
}
}
28 changes: 14 additions & 14 deletions rust/ql/lib/ext/generated/reqwest.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -424,21 +424,21 @@ extensions:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["<reqwest::async_impl::client::Client>::delete", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::get", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::head", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::patch", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::post", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::put", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::async_impl::client::Client>::delete", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::client::Client>::get", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::client::Client>::head", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::client::Client>::patch", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::client::Client>::post", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::client::Client>::put", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::async_impl::multipart::Form>::into_stream", "Argument[self]", "log-injection", "df-generated"]
- ["<reqwest::async_impl::multipart::Form>::stream", "Argument[self]", "log-injection", "df-generated"]
- ["<reqwest::async_impl::request::RequestBuilder>::multipart", "Argument[0]", "log-injection", "df-generated"]
- ["<reqwest::blocking::client::Client>::delete", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::get", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::head", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::patch", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::post", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::put", "Argument[0]", "transmission", "df-generated"]
- ["<reqwest::blocking::client::Client>::delete", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::client::Client>::get", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::client::Client>::head", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::client::Client>::patch", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::client::Client>::post", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::client::Client>::put", "Argument[0]", "request-url", "df-generated"]
- ["<reqwest::blocking::multipart::Form>::into_reader", "Argument[self]", "log-injection", "df-generated"]
- ["<reqwest::blocking::multipart::Form>::reader", "Argument[self]", "log-injection", "df-generated"]
- ["<reqwest::blocking::multipart::Reader as std::io::Read>::read", "Argument[self]", "log-injection", "df-generated"]
Expand All @@ -450,9 +450,9 @@ extensions:
- ["<reqwest::blocking::response::Response>::text_with_charset", "Argument[self]", "pointer-access", "df-generated"]
- ["<reqwest::connect::ConnectorService as tower_service::Service>::call", "Argument[0]", "log-injection", "df-generated"]
- ["<reqwest::error::Error>::new", "Argument[1]", "pointer-access", "df-generated"]
- ["reqwest::blocking::get", "Argument[0]", "transmission", "df-generated"]
- ["reqwest::blocking::get", "Argument[0]", "request-url", "df-generated"]
- ["reqwest::blocking::wait::timeout", "Argument[1]", "pointer-access", "df-generated"]
- ["reqwest::get", "Argument[0]", "transmission", "df-generated"]
- ["reqwest::get", "Argument[0]", "request-url", "df-generated"]
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
Expand Down
4 changes: 4 additions & 0 deletions rust/ql/src/change-notes/2025-09-09-request-forgery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rust/request-forgery`, for detecting server-side request forgery vulnerabilities.
48 changes: 48 additions & 0 deletions rust/ql/src/queries/security/CWE-918/RequestForgery.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>
Directly incorporating user input into an HTTP request without validating the
input can facilitate server-side request forgery (SSRF) attacks. In these
attacks, the server may be tricked into making a request to an unintended API
endpoint or resource.

If the server is connected to an internal network, attackers can bypass security
boundaries to target internal services.

Forged requests can execute unintended actions, leak data if redirected to an
external server, or compromise the server if responses are handled insecurely.
</p>
</overview>

<recommendation>
<p>
To guard against SSRF attacks, you should avoid putting user-provided input
directly into a request URL. Instead, maintain a list of authorized URLs on the
server; then choose from that list based on the input provided. Alternatively,
ensure requests constructed from user input are limited to a particular host or
a more restrictive URL prefix.
</p>
</recommendation>

<example>
<p>
The following example shows an HTTP request parameter being used directly to
form a new request without validating the input, which facilitates SSRF attacks.
It also shows how to remedy the problem by validating the user input against a
known fixed string.
</p>

<sample src="RequestForgery.rs" />
</example>

<references>
<li>
<a href="https://owasp.org/www-community/attacks/Server_Side_Request_Forgery">OWASP Server Side Request Forgery</a>.
</li>
</references>

</qhelp>
38 changes: 38 additions & 0 deletions rust/ql/src/queries/security/CWE-918/RequestForgery.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @name Server-side request forgery
* @description Making a network request with user-controlled data in the URL allows for request forgery attacks.
* @kind path-problem
* @problem.severity error
* @security-severity 9.1
* @precision high
* @id rust/request-forgery
* @tags security
* external/cwe/cwe-918
*/

private import rust
private import codeql.rust.dataflow.TaintTracking
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.security.RequestForgeryExtensions

/**
* A taint-tracking configuration for detecting request forgery vulnerabilities.
*/
module RequestForgeryConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RequestForgery::Source }

predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgery::Sink }

predicate isBarrier(DataFlow::Node barrier) { barrier instanceof RequestForgery::Barrier }

predicate observeDiffInformedIncrementalMode() { any() }
}

module RequestForgeryFlow = TaintTracking::Global<RequestForgeryConfig>;

import RequestForgeryFlow::PathGraph

from RequestForgeryFlow::PathNode source, RequestForgeryFlow::PathNode sink
where RequestForgeryFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "The URL of this request depends on a $@.", source.getNode(),
"user-provided value"
39 changes: 39 additions & 0 deletions rust/ql/src/queries/security/CWE-918/RequestForgery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// BAD: Endpoint handler that makes requests based on user input
async fn vulnerable_endpoint_handler(req: Request) -> Result<Response> {
// This request is vulnerable to SSRF attacks as the user controls the
// entire URL
let response = reqwest::get(&req.user_url).await;

match response {
Ok(resp) => {
let body = resp.text().await.unwrap_or_default();
Ok(Response {
message: "Success".to_string(),
data: body,
})
}
Err(_) => Err("Request failed")
}
}

// GOOD: Validate user input against an allowlist
async fn secure_endpoint_handler(req: Request) -> Result<Response> {
// Allow list of specific, known-safe URLs
let allowed_hosts = ["api.example.com", "trusted-service.com"];

if !allowed_hosts.contains(&req.user_url) {
return Err("Untrusted domain");
}
// This request is safe as the user input has been validated
let response = reqwest::get(&req.user_url).await;
match response {
Ok(resp) => {
let body = resp.text().await.unwrap_or_default();
Ok(Response {
message: "Success".to_string(),
data: body,
})
}
Err(_) => Err("Request failed")
}
}
1 change: 1 addition & 0 deletions rust/ql/src/queries/summary/Stats.qll
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private import codeql.rust.security.AccessInvalidPointerExtensions
private import codeql.rust.security.CleartextLoggingExtensions
private import codeql.rust.security.CleartextStorageDatabaseExtensions
private import codeql.rust.security.CleartextTransmissionExtensions
private import codeql.rust.security.RequestForgeryExtensions
private import codeql.rust.security.LogInjectionExtensions
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.TaintedPathExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ edges
| main.rs:33:50:33:57 | password | main.rs:33:23:33:57 | MacroExpr | provenance | |
| main.rs:35:33:35:35 | url | main.rs:35:12:35:18 | request | provenance | MaD:2 Sink:MaD:2 |
models
| 1 | Sink: <reqwest::async_impl::client::Client>::post; Argument[0]; transmission |
| 2 | Sink: <reqwest::async_impl::client::Client>::request; Argument[1]; transmission |
| 3 | Sink: <reqwest::blocking::client::Client>::request; Argument[1]; transmission |
| 4 | Sink: reqwest::blocking::get; Argument[0]; transmission |
| 1 | Sink: <reqwest::async_impl::client::Client>::post; Argument[0]; request-url |
| 2 | Sink: <reqwest::async_impl::client::Client>::request; Argument[1]; request-url |
| 3 | Sink: <reqwest::blocking::client::Client>::request; Argument[1]; request-url |
| 4 | Sink: reqwest::blocking::get; Argument[0]; request-url |
| 5 | Summary: <core::result::Result>::unwrap; Argument[self].Field[core::result::Result::Ok(0)]; ReturnValue; value |
| 6 | Summary: <url::Url>::parse; Argument[0].Reference; ReturnValue.Field[core::result::Result::Ok(0)]; taint |
| 7 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
multipleCallTargets
| request_forgery_tests.rs:30:36:30:51 | user_url.as_str() |
| request_forgery_tests.rs:30:36:30:52 | user_url.as_str() |
Loading