Skip to content

Commit a2a9575

Browse files
committed
Add tests for safe URL flow
1 parent 5b07e8c commit a2a9575

File tree

7 files changed

+276
-4
lines changed

7 files changed

+276
-4
lines changed

go/ql/lib/semmle/go/security/OpenUrlRedirectCustomizations.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import go
88
import UrlConcatenation
9-
import SafeUrlFlowCustomizations
9+
private import SafeUrlFlowCustomizations
1010
import semmle.go.dataflow.barrierguardutil.RedirectCheckBarrierGuard
1111
import semmle.go.dataflow.barrierguardutil.RegexpCheck
1212
import semmle.go.dataflow.barrierguardutil.UrlCheck

go/ql/lib/semmle/go/security/RequestForgeryCustomizations.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import go
66
import UrlConcatenation
7-
import SafeUrlFlowCustomizations
7+
private import SafeUrlFlowCustomizations
88
import semmle.go.dataflow.barrierguardutil.RedirectCheckBarrierGuard
99
import semmle.go.dataflow.barrierguardutil.RegexpCheck
1010
import semmle.go.dataflow.barrierguardutil.UrlCheck

go/ql/lib/semmle/go/security/SafeUrlFlow.qll

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ module SafeUrlFlow {
3030

3131
predicate isBarrierOut(DataFlow::Node node) {
3232
// block propagation of this safe value when its host is overwritten
33-
exists(Write w, Field f | f.hasQualifiedName("net/url", "URL", "Host") |
34-
w.writesField(node.getASuccessor(), f, _)
33+
exists(Write w, DataFlow::Node b, Field f |
34+
f.hasQualifiedName("net/url", "URL", "Host") and
35+
b = node.getASuccessor() and
36+
w.writesField(b, f, _)
3537
)
3638
or
3739
node instanceof SanitizerEdge
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#select
2+
| SafeUrlFlow.go:11:24:11:46 | ...+... | SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:11:24:11:46 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:10:10:10:17 | selection of Host | here |
3+
| SafeUrlFlow.go:14:29:14:44 | call to String | SafeUrlFlow.go:13:13:13:19 | selection of URL | SafeUrlFlow.go:14:29:14:44 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:13:13:13:19 | selection of URL | here |
4+
| SafeUrlFlow.go:18:11:18:28 | call to String | SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:18:11:18:28 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:10:10:10:17 | selection of Host | here |
5+
| SafeUrlFlow.go:49:24:49:57 | ...+... | SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:49:24:49:57 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:39:13:39:19 | selection of URL | here |
6+
| SafeUrlFlow.go:50:29:50:51 | ...+... | SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:50:29:50:51 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:39:13:39:19 | selection of URL | here |
7+
| SafeUrlFlow.go:51:11:51:38 | ...+... | SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:51:11:51:38 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:39:13:39:19 | selection of URL | here |
8+
| SafeUrlFlow.go:60:11:60:26 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:60:11:60:26 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
9+
| SafeUrlFlow.go:61:12:61:27 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:61:12:61:27 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
10+
| SafeUrlFlow.go:62:16:62:31 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:62:16:62:31 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
11+
| SafeUrlFlow.go:63:12:63:27 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:63:12:63:27 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
12+
| SafeUrlFlow.go:67:13:67:28 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:67:13:67:28 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
13+
| SafeUrlFlow.go:68:14:68:29 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:68:14:68:29 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
14+
| SafeUrlFlow.go:69:18:69:33 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:69:18:69:33 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
15+
| SafeUrlFlow.go:70:14:70:29 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:70:14:70:29 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
16+
| SafeUrlFlow.go:73:39:73:54 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:73:39:73:54 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
17+
| SafeUrlFlow.go:77:70:77:85 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:77:70:77:85 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
18+
| SafeUrlFlow.go:81:40:81:55 | call to String | SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:81:40:81:55 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:57:13:57:19 | selection of URL | here |
19+
| SafeUrlFlow.go:94:24:94:41 | call to String | SafeUrlFlow.go:87:14:87:21 | selection of Host | SafeUrlFlow.go:94:24:94:41 | call to String | A safe URL flows here from $@. | SafeUrlFlow.go:87:14:87:21 | selection of Host | here |
20+
| SafeUrlFlow.go:116:11:116:23 | reconstructed | SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:116:11:116:23 | reconstructed | A safe URL flows here from $@. | SafeUrlFlow.go:106:13:106:19 | selection of URL | here |
21+
| SafeUrlFlow.go:119:24:119:46 | ...+... | SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:119:24:119:46 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:106:13:106:19 | selection of URL | here |
22+
| SafeUrlFlow.go:120:29:120:54 | ...+... | SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:120:29:120:54 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:106:13:106:19 | selection of URL | here |
23+
| SafeUrlFlow.go:121:12:121:38 | ...+... | SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:121:12:121:38 | ...+... | A safe URL flows here from $@. | SafeUrlFlow.go:106:13:106:19 | selection of URL | here |
24+
edges
25+
| SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:11:24:11:46 | ...+... | provenance | Sink:MaD:1 |
26+
| SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:17:19:17:22 | host | provenance | |
27+
| SafeUrlFlow.go:13:13:13:19 | selection of URL | SafeUrlFlow.go:14:29:14:35 | baseURL | provenance | Src:MaD:2 |
28+
| SafeUrlFlow.go:14:29:14:35 | baseURL | SafeUrlFlow.go:14:29:14:44 | call to String | provenance | MaD:3 |
29+
| SafeUrlFlow.go:17:19:17:22 | host | SafeUrlFlow.go:18:11:18:19 | targetURL | provenance | Config |
30+
| SafeUrlFlow.go:18:11:18:19 | targetURL | SafeUrlFlow.go:18:11:18:28 | call to String | provenance | MaD:3 |
31+
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:49:24:49:57 | ...+... | provenance | Src:MaD:2 Sink:MaD:1 |
32+
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:50:29:50:51 | ...+... | provenance | Src:MaD:2 |
33+
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:51:11:51:38 | ...+... | provenance | Src:MaD:2 |
34+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:60:11:60:17 | baseURL | provenance | Src:MaD:2 |
35+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:61:12:61:18 | baseURL | provenance | Src:MaD:2 |
36+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:62:16:62:22 | baseURL | provenance | Src:MaD:2 |
37+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:63:12:63:18 | baseURL | provenance | Src:MaD:2 |
38+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:67:13:67:19 | baseURL | provenance | Src:MaD:2 |
39+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:68:14:68:20 | baseURL | provenance | Src:MaD:2 |
40+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:69:18:69:24 | baseURL | provenance | Src:MaD:2 |
41+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:70:14:70:20 | baseURL | provenance | Src:MaD:2 |
42+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:73:39:73:45 | baseURL | provenance | Src:MaD:2 |
43+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:77:70:77:76 | baseURL | provenance | Src:MaD:2 |
44+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:81:40:81:46 | baseURL | provenance | Src:MaD:2 |
45+
| SafeUrlFlow.go:60:11:60:17 | baseURL | SafeUrlFlow.go:60:11:60:26 | call to String | provenance | MaD:3 |
46+
| SafeUrlFlow.go:61:12:61:18 | baseURL | SafeUrlFlow.go:61:12:61:27 | call to String | provenance | MaD:3 |
47+
| SafeUrlFlow.go:62:16:62:22 | baseURL | SafeUrlFlow.go:62:16:62:31 | call to String | provenance | MaD:3 |
48+
| SafeUrlFlow.go:63:12:63:18 | baseURL | SafeUrlFlow.go:63:12:63:27 | call to String | provenance | MaD:3 |
49+
| SafeUrlFlow.go:67:13:67:19 | baseURL | SafeUrlFlow.go:67:13:67:28 | call to String | provenance | MaD:3 |
50+
| SafeUrlFlow.go:68:14:68:20 | baseURL | SafeUrlFlow.go:68:14:68:29 | call to String | provenance | MaD:3 |
51+
| SafeUrlFlow.go:69:18:69:24 | baseURL | SafeUrlFlow.go:69:18:69:33 | call to String | provenance | MaD:3 |
52+
| SafeUrlFlow.go:70:14:70:20 | baseURL | SafeUrlFlow.go:70:14:70:29 | call to String | provenance | MaD:3 |
53+
| SafeUrlFlow.go:73:39:73:45 | baseURL | SafeUrlFlow.go:73:39:73:54 | call to String | provenance | MaD:3 |
54+
| SafeUrlFlow.go:77:70:77:76 | baseURL | SafeUrlFlow.go:77:70:77:85 | call to String | provenance | MaD:3 |
55+
| SafeUrlFlow.go:81:40:81:46 | baseURL | SafeUrlFlow.go:81:40:81:55 | call to String | provenance | MaD:3 |
56+
| SafeUrlFlow.go:87:14:87:21 | selection of Host | SafeUrlFlow.go:91:19:91:26 | safeHost | provenance | |
57+
| SafeUrlFlow.go:91:19:91:26 | safeHost | SafeUrlFlow.go:94:24:94:32 | targetURL | provenance | Config |
58+
| SafeUrlFlow.go:94:24:94:32 | targetURL | SafeUrlFlow.go:94:24:94:41 | call to String | provenance | MaD:3 Sink:MaD:1 |
59+
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:116:11:116:23 | reconstructed | provenance | Src:MaD:2 |
60+
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:119:24:119:46 | ...+... | provenance | Src:MaD:2 Sink:MaD:1 |
61+
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:120:29:120:54 | ...+... | provenance | Src:MaD:2 |
62+
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:121:12:121:38 | ...+... | provenance | Src:MaD:2 |
63+
models
64+
| 1 | Sink: net/http; ; false; Redirect; ; ; Argument[2]; url-redirection[0]; manual |
65+
| 2 | Source: net/http; Request; true; URL; ; ; ; remote; manual |
66+
| 3 | Summary: fmt; Stringer; true; String; ; ; Argument[receiver]; ReturnValue; taint; manual |
67+
nodes
68+
| SafeUrlFlow.go:10:10:10:17 | selection of Host | semmle.label | selection of Host |
69+
| SafeUrlFlow.go:11:24:11:46 | ...+... | semmle.label | ...+... |
70+
| SafeUrlFlow.go:13:13:13:19 | selection of URL | semmle.label | selection of URL |
71+
| SafeUrlFlow.go:14:29:14:35 | baseURL | semmle.label | baseURL |
72+
| SafeUrlFlow.go:14:29:14:44 | call to String | semmle.label | call to String |
73+
| SafeUrlFlow.go:17:19:17:22 | host | semmle.label | host |
74+
| SafeUrlFlow.go:18:11:18:19 | targetURL | semmle.label | targetURL |
75+
| SafeUrlFlow.go:18:11:18:28 | call to String | semmle.label | call to String |
76+
| SafeUrlFlow.go:39:13:39:19 | selection of URL | semmle.label | selection of URL |
77+
| SafeUrlFlow.go:49:24:49:57 | ...+... | semmle.label | ...+... |
78+
| SafeUrlFlow.go:50:29:50:51 | ...+... | semmle.label | ...+... |
79+
| SafeUrlFlow.go:51:11:51:38 | ...+... | semmle.label | ...+... |
80+
| SafeUrlFlow.go:57:13:57:19 | selection of URL | semmle.label | selection of URL |
81+
| SafeUrlFlow.go:60:11:60:17 | baseURL | semmle.label | baseURL |
82+
| SafeUrlFlow.go:60:11:60:26 | call to String | semmle.label | call to String |
83+
| SafeUrlFlow.go:61:12:61:18 | baseURL | semmle.label | baseURL |
84+
| SafeUrlFlow.go:61:12:61:27 | call to String | semmle.label | call to String |
85+
| SafeUrlFlow.go:62:16:62:22 | baseURL | semmle.label | baseURL |
86+
| SafeUrlFlow.go:62:16:62:31 | call to String | semmle.label | call to String |
87+
| SafeUrlFlow.go:63:12:63:18 | baseURL | semmle.label | baseURL |
88+
| SafeUrlFlow.go:63:12:63:27 | call to String | semmle.label | call to String |
89+
| SafeUrlFlow.go:67:13:67:19 | baseURL | semmle.label | baseURL |
90+
| SafeUrlFlow.go:67:13:67:28 | call to String | semmle.label | call to String |
91+
| SafeUrlFlow.go:68:14:68:20 | baseURL | semmle.label | baseURL |
92+
| SafeUrlFlow.go:68:14:68:29 | call to String | semmle.label | call to String |
93+
| SafeUrlFlow.go:69:18:69:24 | baseURL | semmle.label | baseURL |
94+
| SafeUrlFlow.go:69:18:69:33 | call to String | semmle.label | call to String |
95+
| SafeUrlFlow.go:70:14:70:20 | baseURL | semmle.label | baseURL |
96+
| SafeUrlFlow.go:70:14:70:29 | call to String | semmle.label | call to String |
97+
| SafeUrlFlow.go:73:39:73:45 | baseURL | semmle.label | baseURL |
98+
| SafeUrlFlow.go:73:39:73:54 | call to String | semmle.label | call to String |
99+
| SafeUrlFlow.go:77:70:77:76 | baseURL | semmle.label | baseURL |
100+
| SafeUrlFlow.go:77:70:77:85 | call to String | semmle.label | call to String |
101+
| SafeUrlFlow.go:81:40:81:46 | baseURL | semmle.label | baseURL |
102+
| SafeUrlFlow.go:81:40:81:55 | call to String | semmle.label | call to String |
103+
| SafeUrlFlow.go:87:14:87:21 | selection of Host | semmle.label | selection of Host |
104+
| SafeUrlFlow.go:91:19:91:26 | safeHost | semmle.label | safeHost |
105+
| SafeUrlFlow.go:94:24:94:32 | targetURL | semmle.label | targetURL |
106+
| SafeUrlFlow.go:94:24:94:41 | call to String | semmle.label | call to String |
107+
| SafeUrlFlow.go:106:13:106:19 | selection of URL | semmle.label | selection of URL |
108+
| SafeUrlFlow.go:116:11:116:23 | reconstructed | semmle.label | reconstructed |
109+
| SafeUrlFlow.go:119:24:119:46 | ...+... | semmle.label | ...+... |
110+
| SafeUrlFlow.go:120:29:120:54 | ...+... | semmle.label | ...+... |
111+
| SafeUrlFlow.go:121:12:121:38 | ...+... | semmle.label | ...+... |
112+
subpaths
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
)
8+
9+
func testStdlibSources(w http.ResponseWriter, req *http.Request) {
10+
host := req.Host // $ Source
11+
http.Redirect(w, req, "https://"+host+"/safe", http.StatusFound) // $ Alert
12+
13+
baseURL := req.URL // $ Source
14+
w.Header().Set("Location", baseURL.String()) // $ Alert
15+
16+
targetURL := url.URL{}
17+
targetURL.Host = host // propagation to URL when Host is assigned
18+
http.Get(targetURL.String()) // $ Alert
19+
}
20+
21+
func testSanitizerEdge1(w http.ResponseWriter, req *http.Request) {
22+
baseURL := req.URL
23+
24+
// SanitizerEdge: Query method call (unsafe URL method - breaks flow)
25+
query := baseURL.Query() // sanitizer edge blocks flow here
26+
http.Redirect(w, req, query.Get("redirect"), http.StatusFound) // no flow expected
27+
}
28+
29+
func testSanitizerEdge2(w http.ResponseWriter, req *http.Request) {
30+
baseURL := req.URL
31+
32+
// SanitizerEdge: String slicing (breaks flow)
33+
urlString := baseURL.String()
34+
sliced := urlString[0:10] // sanitizer edge blocks flow here
35+
w.Header().Set("Location", sliced) // no flow expected
36+
}
37+
38+
func testFieldReads(w http.ResponseWriter, req *http.Request) {
39+
baseURL := req.URL // $ Source
40+
41+
// Test that other URL methods preserve flow
42+
scheme := baseURL.Scheme // should preserve flow
43+
host := baseURL.Host // should preserve flow
44+
path := baseURL.Path // should preserve flow
45+
fragment := baseURL.Fragment // should preserve flow
46+
user := baseURL.User // should preserve flow (but unsafe field)
47+
48+
// These should still have flow (not sanitized)
49+
http.Redirect(w, req, "https://"+scheme+"://example.com", http.StatusFound) // $ Alert
50+
w.Header().Set("Location", "https://"+host+"/safe") // $ Alert
51+
http.Get("https://example.com" + path) // $ Alert
52+
http.Get(fragment)
53+
http.Get(user.String())
54+
}
55+
56+
func testRequestForgerySinks(req *http.Request) {
57+
baseURL := req.URL // $ Source
58+
59+
// Standard library HTTP functions (request-forgery sinks)
60+
http.Get(baseURL.String()) // $ Alert
61+
http.Post(baseURL.String(), "application/json", nil) // $ Alert
62+
http.PostForm(baseURL.String(), nil) // $ Alert
63+
http.Head(baseURL.String()) // $ Alert
64+
65+
// HTTP Client methods (request-forgery sinks)
66+
client := &http.Client{}
67+
client.Get(baseURL.String()) // $ Alert
68+
client.Post(baseURL.String(), "application/json", nil) // $ Alert
69+
client.PostForm(baseURL.String(), nil) // $ Alert
70+
client.Head(baseURL.String()) // $ Alert
71+
72+
// NewRequest + Client.Do (request-forgery sinks)
73+
request, _ := http.NewRequest("GET", baseURL.String(), nil) // $ Alert
74+
client.Do(request)
75+
76+
// NewRequestWithContext + Client.Do (request-forgery sinks)
77+
reqWithCtx, _ := http.NewRequestWithContext(context.TODO(), "POST", baseURL.String(), nil) // $ Alert
78+
client.Do(reqWithCtx)
79+
80+
// RoundTrip method (request-forgery sink)
81+
request2, _ := http.NewRequest("GET", baseURL.String(), nil) // $ Alert
82+
transport := &http.Transport{}
83+
transport.RoundTrip(request2)
84+
}
85+
86+
func testHostFieldAssignmentFlow(w http.ResponseWriter, req *http.Request) {
87+
safeHost := req.Host // $ Source
88+
89+
// Test additional flow step: propagation when Host field is assigned
90+
targetURL, _ := url.Parse("http://example.com/data")
91+
targetURL.Host = safeHost // additional flow step from SafeUrlFlow config
92+
93+
// Flow should propagate to the whole URL after Host assignment
94+
http.Redirect(w, req, targetURL.String(), http.StatusFound) // $ Alert
95+
}
96+
97+
func testHostFieldOverwritten(w http.ResponseWriter, req *http.Request) {
98+
baseURL := req.URL
99+
100+
// Flow should be blocked when Host is overwritten
101+
baseURL.Host = "something.else.com"
102+
http.Get(baseURL.String())
103+
}
104+
105+
func testFieldAccess(w http.ResponseWriter, req *http.Request) {
106+
baseURL := req.URL // $ Source
107+
108+
// Safe field accesses that should preserve flow
109+
host := baseURL.Host
110+
path := baseURL.Path
111+
scheme := baseURL.Scheme
112+
opaquePart := baseURL.Opaque
113+
114+
// Reconstruct URL - flow should be preserved through field access
115+
reconstructed := scheme + "://" + host + path
116+
http.Get(reconstructed) // $ Alert
117+
118+
// Test individual fields
119+
http.Redirect(w, req, "https://"+host+"/path", http.StatusFound) // $ Alert
120+
w.Header().Set("Location", "https://example.com"+path) // $ Alert
121+
http.Post(scheme+"://example.com/api", "application/json", nil) // $ Alert
122+
use(opaquePart) // avoid unused variable warning
123+
124+
// Unsafe field accesses that should be sanitized by UnsafeFieldReadSanitizer
125+
// These read unsafe URL fields and should NOT have flow
126+
unsafeUser := baseURL.User // sanitizer edge (User field)
127+
unsafeQuery := baseURL.RawQuery // sanitizer edge (RawQuery field)
128+
unsafeFragment := baseURL.Fragment // sanitizer edge (Fragment field)
129+
130+
// These should NOT have flow due to sanitizer edges
131+
if unsafeUser != nil {
132+
http.Redirect(w, req, unsafeUser.String(), http.StatusFound) // no flow expected
133+
}
134+
w.Header().Set("Location", "https://example.com/?"+unsafeQuery) // no flow expected
135+
http.Get("https://example.com/#" + unsafeFragment) // no flow expected
136+
}
137+
138+
// Helper function to avoid unused variable warnings
139+
func use(vars ...interface{}) {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @id go/test-safe-url-flow
3+
* @kind path-problem
4+
* @problem.severity recommendation
5+
*/
6+
7+
import go
8+
import semmle.go.security.RequestForgeryCustomizations
9+
import semmle.go.security.OpenUrlRedirectCustomizations
10+
import semmle.go.security.SafeUrlFlow
11+
import SafeUrlFlow::Flow::PathGraph
12+
13+
from SafeUrlFlow::Flow::PathNode source, SafeUrlFlow::Flow::PathNode sink
14+
where SafeUrlFlow::Flow::flowPath(source, sink)
15+
select sink.getNode(), source, sink, "A safe URL flows here from $@.", source.getNode(), "here"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
query: SafeUrlFlow.ql
2+
postprocess:
3+
- utils/test/PrettyPrintModels.ql
4+
- utils/test/InlineExpectationsTestQuery.ql

0 commit comments

Comments
 (0)