Skip to content

Go: Add Head and Client.Head from net/http as request forgery sinks #20000

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions go/ql/lib/change-notes/2025-07-08-request-forgery-sinks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Added models for the `Head` function and the `Client.Head` method, from the `net/http` package, to the `Http::ClientRequest` class. This means that they will be recognized as sinks for the query `go/request-forgery` and the experimental query `go/ssrf`.
11 changes: 5 additions & 6 deletions go/ql/lib/semmle/go/frameworks/stdlib/NetHttp.qll
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,11 @@ module NetHttp {
private class RequestCall extends Http::ClientRequest::Range, DataFlow::CallNode {
RequestCall() {
exists(string functionName |
(
this.getTarget().hasQualifiedName("net/http", functionName)
or
this.getTarget().(Method).hasQualifiedName("net/http", "Client", functionName)
) and
(functionName = "Get" or functionName = "Post" or functionName = "PostForm")
this.getTarget().hasQualifiedName("net/http", functionName)
or
this.getTarget().(Method).hasQualifiedName("net/http", "Client", functionName)
|
functionName = ["Get", "Head", "Post", "PostForm"]
)
}

Expand Down
92 changes: 55 additions & 37 deletions go/ql/test/query-tests/Security/CWE-918/RequestForgery.expected
Original file line number Diff line number Diff line change
@@ -1,49 +1,61 @@
#select
| RequestForgery.go:11:15:11:66 | call to Get | RequestForgery.go:8:12:8:34 | call to FormValue | RequestForgery.go:11:24:11:65 | ...+... | The $@ of this request depends on a $@. | RequestForgery.go:11:24:11:65 | ...+... | URL | RequestForgery.go:8:12:8:34 | call to FormValue | user-provided value |
| tst.go:14:2:14:18 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:14:11:14:17 | tainted | The $@ of this request depends on a $@. | tst.go:14:11:14:17 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:16:2:16:19 | call to Head | tst.go:10:13:10:35 | call to FormValue | tst.go:16:12:16:18 | tainted | The $@ of this request depends on a $@. | tst.go:16:12:16:18 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:18:2:18:38 | call to Post | tst.go:10:13:10:35 | call to FormValue | tst.go:18:12:18:18 | tainted | The $@ of this request depends on a $@. | tst.go:18:12:18:18 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:22:2:22:14 | call to Do | tst.go:10:13:10:35 | call to FormValue | tst.go:21:34:21:40 | tainted | The $@ of this request depends on a $@. | tst.go:21:34:21:40 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:25:2:25:14 | call to Do | tst.go:10:13:10:35 | call to FormValue | tst.go:24:66:24:72 | tainted | The $@ of this request depends on a $@. | tst.go:24:66:24:72 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:27:2:27:30 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:27:11:27:29 | ...+... | The $@ of this request depends on a $@. | tst.go:27:11:27:29 | ...+... | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:29:2:29:41 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:29:11:29:40 | ...+... | The $@ of this request depends on a $@. | tst.go:29:11:29:40 | ...+... | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:37:2:37:21 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:37:11:37:20 | call to String | The $@ of this request depends on a $@. | tst.go:37:11:37:20 | call to String | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:20:2:20:28 | call to PostForm | tst.go:10:13:10:35 | call to FormValue | tst.go:20:16:20:22 | tainted | The $@ of this request depends on a $@. | tst.go:20:16:20:22 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:24:2:24:15 | call to Do | tst.go:10:13:10:35 | call to FormValue | tst.go:23:35:23:41 | tainted | The $@ of this request depends on a $@. | tst.go:23:35:23:41 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:27:2:27:15 | call to Do | tst.go:10:13:10:35 | call to FormValue | tst.go:26:68:26:74 | tainted | The $@ of this request depends on a $@. | tst.go:26:68:26:74 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:29:2:29:20 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:29:13:29:19 | tainted | The $@ of this request depends on a $@. | tst.go:29:13:29:19 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:30:2:30:21 | call to Head | tst.go:10:13:10:35 | call to FormValue | tst.go:30:14:30:20 | tainted | The $@ of this request depends on a $@. | tst.go:30:14:30:20 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:31:2:31:40 | call to Post | tst.go:10:13:10:35 | call to FormValue | tst.go:31:14:31:20 | tainted | The $@ of this request depends on a $@. | tst.go:31:14:31:20 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:32:2:32:30 | call to PostForm | tst.go:10:13:10:35 | call to FormValue | tst.go:32:18:32:24 | tainted | The $@ of this request depends on a $@. | tst.go:32:18:32:24 | tainted | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:34:2:34:30 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:34:11:34:29 | ...+... | The $@ of this request depends on a $@. | tst.go:34:11:34:29 | ...+... | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:36:2:36:41 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:36:11:36:40 | ...+... | The $@ of this request depends on a $@. | tst.go:36:11:36:40 | ...+... | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| tst.go:44:2:44:21 | call to Get | tst.go:10:13:10:35 | call to FormValue | tst.go:44:11:44:20 | call to String | The $@ of this request depends on a $@. | tst.go:44:11:44:20 | call to String | URL | tst.go:10:13:10:35 | call to FormValue | user-provided value |
| websocket.go:65:12:65:53 | call to Dial | websocket.go:60:21:60:31 | call to Referer | websocket.go:65:27:65:40 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:65:27:65:40 | untrustedInput | WebSocket URL | websocket.go:60:21:60:31 | call to Referer | user-provided value |
| websocket.go:79:13:79:40 | call to DialConfig | websocket.go:74:21:74:31 | call to Referer | websocket.go:78:36:78:49 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:78:36:78:49 | untrustedInput | WebSocket URL | websocket.go:74:21:74:31 | call to Referer | user-provided value |
| websocket.go:91:3:91:50 | call to Dial | websocket.go:88:21:88:31 | call to Referer | websocket.go:91:31:91:44 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:91:31:91:44 | untrustedInput | WebSocket URL | websocket.go:88:21:88:31 | call to Referer | user-provided value |
| websocket.go:110:3:110:39 | call to Dial | websocket.go:107:21:107:31 | call to Referer | websocket.go:110:15:110:28 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:110:15:110:28 | untrustedInput | WebSocket URL | websocket.go:107:21:107:31 | call to Referer | user-provided value |
| websocket.go:129:3:129:62 | call to DialContext | websocket.go:126:21:126:31 | call to Referer | websocket.go:129:38:129:51 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:129:38:129:51 | untrustedInput | WebSocket URL | websocket.go:126:21:126:31 | call to Referer | user-provided value |
| websocket.go:155:3:155:45 | call to Dial | websocket.go:154:21:154:31 | call to Referer | websocket.go:155:31:155:44 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:155:31:155:44 | untrustedInput | WebSocket URL | websocket.go:154:21:154:31 | call to Referer | user-provided value |
| websocket.go:162:3:162:45 | call to Dial | websocket.go:160:21:160:31 | call to Referer | websocket.go:162:31:162:44 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:162:31:162:44 | untrustedInput | WebSocket URL | websocket.go:160:21:160:31 | call to Referer | user-provided value |
| websocket.go:197:3:197:32 | call to BuildProxy | websocket.go:195:21:195:31 | call to Referer | websocket.go:197:18:197:31 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:197:18:197:31 | untrustedInput | WebSocket URL | websocket.go:195:21:195:31 | call to Referer | user-provided value |
| websocket.go:204:3:204:25 | call to New | websocket.go:202:21:202:31 | call to Referer | websocket.go:204:11:204:24 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:204:11:204:24 | untrustedInput | WebSocket URL | websocket.go:202:21:202:31 | call to Referer | user-provided value |
| websocket.go:197:7:197:36 | call to BuildProxy | websocket.go:195:21:195:31 | call to Referer | websocket.go:197:22:197:35 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:197:22:197:35 | untrustedInput | WebSocket URL | websocket.go:195:21:195:31 | call to Referer | user-provided value |
| websocket.go:204:7:204:29 | call to New | websocket.go:202:21:202:31 | call to Referer | websocket.go:204:15:204:28 | untrustedInput | The $@ of this request depends on a $@. | websocket.go:204:15:204:28 | untrustedInput | WebSocket URL | websocket.go:202:21:202:31 | call to Referer | user-provided value |
edges
| RequestForgery.go:8:12:8:34 | call to FormValue | RequestForgery.go:11:24:11:65 | ...+... | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:14:11:14:17 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:16:12:16:18 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:18:12:18:18 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:21:34:21:40 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:24:66:24:72 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:27:11:27:29 | ...+... | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:29:11:29:40 | ...+... | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:36:11:36:17 | tainted | provenance | Src:MaD:1 |
| tst.go:35:2:35:2 | definition of u [pointer] | tst.go:36:2:36:2 | u [pointer] | provenance | |
| tst.go:36:2:36:2 | implicit dereference | tst.go:35:2:35:2 | definition of u [pointer] | provenance | |
| tst.go:36:2:36:2 | implicit dereference | tst.go:36:2:36:2 | u | provenance | |
| tst.go:36:2:36:2 | implicit dereference | tst.go:37:11:37:11 | u | provenance | |
| tst.go:36:2:36:2 | u | tst.go:36:2:36:2 | implicit dereference | provenance | |
| tst.go:36:2:36:2 | u | tst.go:37:11:37:11 | u | provenance | |
| tst.go:36:2:36:2 | u [pointer] | tst.go:36:2:36:2 | implicit dereference | provenance | |
| tst.go:36:11:36:17 | tainted | tst.go:36:2:36:2 | u | provenance | Config |
| tst.go:36:11:36:17 | tainted | tst.go:37:11:37:11 | u | provenance | Config |
| tst.go:37:11:37:11 | u | tst.go:37:11:37:20 | call to String | provenance | MaD:3 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:20:16:20:22 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:23:35:23:41 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:26:68:26:74 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:29:13:29:19 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:30:14:30:20 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:31:14:31:20 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:32:18:32:24 | tainted | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:34:11:34:29 | ...+... | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:36:11:36:40 | ...+... | provenance | Src:MaD:1 |
| tst.go:10:13:10:35 | call to FormValue | tst.go:43:11:43:17 | tainted | provenance | Src:MaD:1 |
| tst.go:42:2:42:2 | definition of u [pointer] | tst.go:43:2:43:2 | u [pointer] | provenance | |
| tst.go:43:2:43:2 | implicit dereference | tst.go:42:2:42:2 | definition of u [pointer] | provenance | |
| tst.go:43:2:43:2 | implicit dereference | tst.go:43:2:43:2 | u | provenance | |
| tst.go:43:2:43:2 | implicit dereference | tst.go:44:11:44:11 | u | provenance | |
| tst.go:43:2:43:2 | u | tst.go:43:2:43:2 | implicit dereference | provenance | |
| tst.go:43:2:43:2 | u | tst.go:44:11:44:11 | u | provenance | |
| tst.go:43:2:43:2 | u [pointer] | tst.go:43:2:43:2 | implicit dereference | provenance | |
| tst.go:43:11:43:17 | tainted | tst.go:43:2:43:2 | u | provenance | Config |
| tst.go:43:11:43:17 | tainted | tst.go:44:11:44:11 | u | provenance | Config |
| tst.go:44:11:44:11 | u | tst.go:44:11:44:20 | call to String | provenance | MaD:3 |
| websocket.go:60:21:60:31 | call to Referer | websocket.go:65:27:65:40 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:74:21:74:31 | call to Referer | websocket.go:78:36:78:49 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:88:21:88:31 | call to Referer | websocket.go:91:31:91:44 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:107:21:107:31 | call to Referer | websocket.go:110:15:110:28 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:126:21:126:31 | call to Referer | websocket.go:129:38:129:51 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:154:21:154:31 | call to Referer | websocket.go:155:31:155:44 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:160:21:160:31 | call to Referer | websocket.go:162:31:162:44 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:195:21:195:31 | call to Referer | websocket.go:197:18:197:31 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:202:21:202:31 | call to Referer | websocket.go:204:11:204:24 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:195:21:195:31 | call to Referer | websocket.go:197:22:197:35 | untrustedInput | provenance | Src:MaD:2 |
| websocket.go:202:21:202:31 | call to Referer | websocket.go:204:15:204:28 | untrustedInput | provenance | Src:MaD:2 |
models
| 1 | Source: net/http; Request; true; FormValue; ; ; ReturnValue; remote; manual |
| 2 | Source: net/http; Request; true; Referer; ; ; ReturnValue; remote; manual |
Expand All @@ -53,18 +65,24 @@ nodes
| RequestForgery.go:11:24:11:65 | ...+... | semmle.label | ...+... |
| tst.go:10:13:10:35 | call to FormValue | semmle.label | call to FormValue |
| tst.go:14:11:14:17 | tainted | semmle.label | tainted |
| tst.go:16:12:16:18 | tainted | semmle.label | tainted |
| tst.go:18:12:18:18 | tainted | semmle.label | tainted |
| tst.go:21:34:21:40 | tainted | semmle.label | tainted |
| tst.go:24:66:24:72 | tainted | semmle.label | tainted |
| tst.go:27:11:27:29 | ...+... | semmle.label | ...+... |
| tst.go:29:11:29:40 | ...+... | semmle.label | ...+... |
| tst.go:35:2:35:2 | definition of u [pointer] | semmle.label | definition of u [pointer] |
| tst.go:36:2:36:2 | implicit dereference | semmle.label | implicit dereference |
| tst.go:36:2:36:2 | u | semmle.label | u |
| tst.go:36:2:36:2 | u [pointer] | semmle.label | u [pointer] |
| tst.go:36:11:36:17 | tainted | semmle.label | tainted |
| tst.go:37:11:37:11 | u | semmle.label | u |
| tst.go:37:11:37:20 | call to String | semmle.label | call to String |
| tst.go:20:16:20:22 | tainted | semmle.label | tainted |
| tst.go:23:35:23:41 | tainted | semmle.label | tainted |
| tst.go:26:68:26:74 | tainted | semmle.label | tainted |
| tst.go:29:13:29:19 | tainted | semmle.label | tainted |
| tst.go:30:14:30:20 | tainted | semmle.label | tainted |
| tst.go:31:14:31:20 | tainted | semmle.label | tainted |
| tst.go:32:18:32:24 | tainted | semmle.label | tainted |
| tst.go:34:11:34:29 | ...+... | semmle.label | ...+... |
| tst.go:36:11:36:40 | ...+... | semmle.label | ...+... |
| tst.go:42:2:42:2 | definition of u [pointer] | semmle.label | definition of u [pointer] |
| tst.go:43:2:43:2 | implicit dereference | semmle.label | implicit dereference |
| tst.go:43:2:43:2 | u | semmle.label | u |
| tst.go:43:2:43:2 | u [pointer] | semmle.label | u [pointer] |
| tst.go:43:11:43:17 | tainted | semmle.label | tainted |
| tst.go:44:11:44:11 | u | semmle.label | u |
| tst.go:44:11:44:20 | call to String | semmle.label | call to String |
| websocket.go:60:21:60:31 | call to Referer | semmle.label | call to Referer |
| websocket.go:65:27:65:40 | untrustedInput | semmle.label | untrustedInput |
| websocket.go:74:21:74:31 | call to Referer | semmle.label | call to Referer |
Expand All @@ -80,7 +98,7 @@ nodes
| websocket.go:160:21:160:31 | call to Referer | semmle.label | call to Referer |
| websocket.go:162:31:162:44 | untrustedInput | semmle.label | untrustedInput |
| websocket.go:195:21:195:31 | call to Referer | semmle.label | call to Referer |
| websocket.go:197:18:197:31 | untrustedInput | semmle.label | untrustedInput |
| websocket.go:197:22:197:35 | untrustedInput | semmle.label | untrustedInput |
| websocket.go:202:21:202:31 | call to Referer | semmle.label | call to Referer |
| websocket.go:204:11:204:24 | untrustedInput | semmle.label | untrustedInput |
| websocket.go:204:15:204:28 | untrustedInput | semmle.label | untrustedInput |
subpaths
4 changes: 2 additions & 2 deletions go/ql/test/query-tests/Security/CWE-918/RequestForgery.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
)

func handler(w http.ResponseWriter, req *http.Request) {
target := req.FormValue("target")
target := req.FormValue("target") // $ Source

// BAD: `target` is controlled by the attacker
resp, err := http.Get("https://" + target + ".example.com/data/")
resp, err := http.Get("https://" + target + ".example.com/data/") // $ Alert
if err != nil {
// error handling
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
query: Security/CWE-918/RequestForgery.ql
postprocess: utils/test/PrettyPrintModels.ql
postprocess:
- utils/test/PrettyPrintModels.ql
- utils/test/InlineExpectationsTestQuery.ql
29 changes: 18 additions & 11 deletions go/ql/test/query-tests/Security/CWE-918/tst.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,41 @@ import (
)

func handler2(w http.ResponseWriter, req *http.Request) {
tainted := req.FormValue("target")
tainted := req.FormValue("target") // $ Source

http.Get("example.com") // OK

http.Get(tainted) // Not OK
http.Get(tainted) // $ Alert

http.Head(tainted) // OK
http.Head(tainted) // $ Alert

http.Post(tainted, "text/basic", nil) // Not OK
http.Post(tainted, "text/basic", nil) // $ Alert

http.PostForm(tainted, nil) // $ Alert

client := &http.Client{}
rq, _ := http.NewRequest("GET", tainted, nil)
client.Do(rq) // Not OK
rq1, _ := http.NewRequest("GET", tainted, nil) // $ Sink
client.Do(rq1) // $ Alert

rq2, _ := http.NewRequestWithContext(context.Background(), "GET", tainted, nil) // $ Sink
client.Do(rq2) // $ Alert

rq, _ = http.NewRequestWithContext(context.Background(), "GET", tainted, nil)
client.Do(rq) // Not OK
client.Get(tainted) // $ Alert
client.Head(tainted) // $ Alert
client.Post(tainted, "text/basic", nil) // $ Alert
client.PostForm(tainted, nil) // $ Alert

http.Get("http://" + tainted) // Not OK
http.Get("http://" + tainted) // $ Alert

http.Get("http://example.com" + tainted) // Not OK
http.Get("http://example.com" + tainted) // $ Alert

http.Get("http://example.com/" + tainted) // OK

http.Get("http://example.com/?" + tainted) // OK

u, _ := url.Parse("http://example.com/relative-path")
u.Host = tainted
http.Get(u.String()) // Not OK
http.Get(u.String()) // $ Alert
}

func main() {
Expand Down
Loading