Add tests for safe URL flow

This commit is contained in:
Owen Mansel-Chan
2025-09-30 11:23:52 +01:00
parent 5b07e8c9c4
commit a2a9575587
7 changed files with 276 additions and 4 deletions

View File

@@ -6,7 +6,7 @@
import go
import UrlConcatenation
import SafeUrlFlowCustomizations
private import SafeUrlFlowCustomizations
import semmle.go.dataflow.barrierguardutil.RedirectCheckBarrierGuard
import semmle.go.dataflow.barrierguardutil.RegexpCheck
import semmle.go.dataflow.barrierguardutil.UrlCheck

View File

@@ -4,7 +4,7 @@
import go
import UrlConcatenation
import SafeUrlFlowCustomizations
private import SafeUrlFlowCustomizations
import semmle.go.dataflow.barrierguardutil.RedirectCheckBarrierGuard
import semmle.go.dataflow.barrierguardutil.RegexpCheck
import semmle.go.dataflow.barrierguardutil.UrlCheck

View File

@@ -30,8 +30,10 @@ module SafeUrlFlow {
predicate isBarrierOut(DataFlow::Node node) {
// block propagation of this safe value when its host is overwritten
exists(Write w, Field f | f.hasQualifiedName("net/url", "URL", "Host") |
w.writesField(node.getASuccessor(), f, _)
exists(Write w, DataFlow::Node b, Field f |
f.hasQualifiedName("net/url", "URL", "Host") and
b = node.getASuccessor() and
w.writesField(b, f, _)
)
or
node instanceof SanitizerEdge

View File

@@ -0,0 +1,112 @@
#select
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
edges
| SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:11:24:11:46 | ...+... | provenance | Sink:MaD:1 |
| SafeUrlFlow.go:10:10:10:17 | selection of Host | SafeUrlFlow.go:17:19:17:22 | host | provenance | |
| SafeUrlFlow.go:13:13:13:19 | selection of URL | SafeUrlFlow.go:14:29:14:35 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:14:29:14:35 | baseURL | SafeUrlFlow.go:14:29:14:44 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:17:19:17:22 | host | SafeUrlFlow.go:18:11:18:19 | targetURL | provenance | Config |
| SafeUrlFlow.go:18:11:18:19 | targetURL | SafeUrlFlow.go:18:11:18:28 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:49:24:49:57 | ...+... | provenance | Src:MaD:2 Sink:MaD:1 |
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:50:29:50:51 | ...+... | provenance | Src:MaD:2 |
| SafeUrlFlow.go:39:13:39:19 | selection of URL | SafeUrlFlow.go:51:11:51:38 | ...+... | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:60:11:60:17 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:61:12:61:18 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:62:16:62:22 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:63:12:63:18 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:67:13:67:19 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:68:14:68:20 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:69:18:69:24 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:70:14:70:20 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:73:39:73:45 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:77:70:77:76 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | SafeUrlFlow.go:81:40:81:46 | baseURL | provenance | Src:MaD:2 |
| SafeUrlFlow.go:60:11:60:17 | baseURL | SafeUrlFlow.go:60:11:60:26 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:61:12:61:18 | baseURL | SafeUrlFlow.go:61:12:61:27 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:62:16:62:22 | baseURL | SafeUrlFlow.go:62:16:62:31 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:63:12:63:18 | baseURL | SafeUrlFlow.go:63:12:63:27 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:67:13:67:19 | baseURL | SafeUrlFlow.go:67:13:67:28 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:68:14:68:20 | baseURL | SafeUrlFlow.go:68:14:68:29 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:69:18:69:24 | baseURL | SafeUrlFlow.go:69:18:69:33 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:70:14:70:20 | baseURL | SafeUrlFlow.go:70:14:70:29 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:73:39:73:45 | baseURL | SafeUrlFlow.go:73:39:73:54 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:77:70:77:76 | baseURL | SafeUrlFlow.go:77:70:77:85 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:81:40:81:46 | baseURL | SafeUrlFlow.go:81:40:81:55 | call to String | provenance | MaD:3 |
| SafeUrlFlow.go:87:14:87:21 | selection of Host | SafeUrlFlow.go:91:19:91:26 | safeHost | provenance | |
| SafeUrlFlow.go:91:19:91:26 | safeHost | SafeUrlFlow.go:94:24:94:32 | targetURL | provenance | Config |
| SafeUrlFlow.go:94:24:94:32 | targetURL | SafeUrlFlow.go:94:24:94:41 | call to String | provenance | MaD:3 Sink:MaD:1 |
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:116:11:116:23 | reconstructed | provenance | Src:MaD:2 |
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:119:24:119:46 | ...+... | provenance | Src:MaD:2 Sink:MaD:1 |
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:120:29:120:54 | ...+... | provenance | Src:MaD:2 |
| SafeUrlFlow.go:106:13:106:19 | selection of URL | SafeUrlFlow.go:121:12:121:38 | ...+... | provenance | Src:MaD:2 |
models
| 1 | Sink: net/http; ; false; Redirect; ; ; Argument[2]; url-redirection[0]; manual |
| 2 | Source: net/http; Request; true; URL; ; ; ; remote; manual |
| 3 | Summary: fmt; Stringer; true; String; ; ; Argument[receiver]; ReturnValue; taint; manual |
nodes
| SafeUrlFlow.go:10:10:10:17 | selection of Host | semmle.label | selection of Host |
| SafeUrlFlow.go:11:24:11:46 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:13:13:13:19 | selection of URL | semmle.label | selection of URL |
| SafeUrlFlow.go:14:29:14:35 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:14:29:14:44 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:17:19:17:22 | host | semmle.label | host |
| SafeUrlFlow.go:18:11:18:19 | targetURL | semmle.label | targetURL |
| SafeUrlFlow.go:18:11:18:28 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:39:13:39:19 | selection of URL | semmle.label | selection of URL |
| SafeUrlFlow.go:49:24:49:57 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:50:29:50:51 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:51:11:51:38 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:57:13:57:19 | selection of URL | semmle.label | selection of URL |
| SafeUrlFlow.go:60:11:60:17 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:60:11:60:26 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:61:12:61:18 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:61:12:61:27 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:62:16:62:22 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:62:16:62:31 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:63:12:63:18 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:63:12:63:27 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:67:13:67:19 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:67:13:67:28 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:68:14:68:20 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:68:14:68:29 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:69:18:69:24 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:69:18:69:33 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:70:14:70:20 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:70:14:70:29 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:73:39:73:45 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:73:39:73:54 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:77:70:77:76 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:77:70:77:85 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:81:40:81:46 | baseURL | semmle.label | baseURL |
| SafeUrlFlow.go:81:40:81:55 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:87:14:87:21 | selection of Host | semmle.label | selection of Host |
| SafeUrlFlow.go:91:19:91:26 | safeHost | semmle.label | safeHost |
| SafeUrlFlow.go:94:24:94:32 | targetURL | semmle.label | targetURL |
| SafeUrlFlow.go:94:24:94:41 | call to String | semmle.label | call to String |
| SafeUrlFlow.go:106:13:106:19 | selection of URL | semmle.label | selection of URL |
| SafeUrlFlow.go:116:11:116:23 | reconstructed | semmle.label | reconstructed |
| SafeUrlFlow.go:119:24:119:46 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:120:29:120:54 | ...+... | semmle.label | ...+... |
| SafeUrlFlow.go:121:12:121:38 | ...+... | semmle.label | ...+... |
subpaths

View File

@@ -0,0 +1,139 @@
package main
import (
"context"
"net/http"
"net/url"
)
func testStdlibSources(w http.ResponseWriter, req *http.Request) {
host := req.Host // $ Source
http.Redirect(w, req, "https://"+host+"/safe", http.StatusFound) // $ Alert
baseURL := req.URL // $ Source
w.Header().Set("Location", baseURL.String()) // $ Alert
targetURL := url.URL{}
targetURL.Host = host // propagation to URL when Host is assigned
http.Get(targetURL.String()) // $ Alert
}
func testSanitizerEdge1(w http.ResponseWriter, req *http.Request) {
baseURL := req.URL
// SanitizerEdge: Query method call (unsafe URL method - breaks flow)
query := baseURL.Query() // sanitizer edge blocks flow here
http.Redirect(w, req, query.Get("redirect"), http.StatusFound) // no flow expected
}
func testSanitizerEdge2(w http.ResponseWriter, req *http.Request) {
baseURL := req.URL
// SanitizerEdge: String slicing (breaks flow)
urlString := baseURL.String()
sliced := urlString[0:10] // sanitizer edge blocks flow here
w.Header().Set("Location", sliced) // no flow expected
}
func testFieldReads(w http.ResponseWriter, req *http.Request) {
baseURL := req.URL // $ Source
// Test that other URL methods preserve flow
scheme := baseURL.Scheme // should preserve flow
host := baseURL.Host // should preserve flow
path := baseURL.Path // should preserve flow
fragment := baseURL.Fragment // should preserve flow
user := baseURL.User // should preserve flow (but unsafe field)
// These should still have flow (not sanitized)
http.Redirect(w, req, "https://"+scheme+"://example.com", http.StatusFound) // $ Alert
w.Header().Set("Location", "https://"+host+"/safe") // $ Alert
http.Get("https://example.com" + path) // $ Alert
http.Get(fragment)
http.Get(user.String())
}
func testRequestForgerySinks(req *http.Request) {
baseURL := req.URL // $ Source
// Standard library HTTP functions (request-forgery sinks)
http.Get(baseURL.String()) // $ Alert
http.Post(baseURL.String(), "application/json", nil) // $ Alert
http.PostForm(baseURL.String(), nil) // $ Alert
http.Head(baseURL.String()) // $ Alert
// HTTP Client methods (request-forgery sinks)
client := &http.Client{}
client.Get(baseURL.String()) // $ Alert
client.Post(baseURL.String(), "application/json", nil) // $ Alert
client.PostForm(baseURL.String(), nil) // $ Alert
client.Head(baseURL.String()) // $ Alert
// NewRequest + Client.Do (request-forgery sinks)
request, _ := http.NewRequest("GET", baseURL.String(), nil) // $ Alert
client.Do(request)
// NewRequestWithContext + Client.Do (request-forgery sinks)
reqWithCtx, _ := http.NewRequestWithContext(context.TODO(), "POST", baseURL.String(), nil) // $ Alert
client.Do(reqWithCtx)
// RoundTrip method (request-forgery sink)
request2, _ := http.NewRequest("GET", baseURL.String(), nil) // $ Alert
transport := &http.Transport{}
transport.RoundTrip(request2)
}
func testHostFieldAssignmentFlow(w http.ResponseWriter, req *http.Request) {
safeHost := req.Host // $ Source
// Test additional flow step: propagation when Host field is assigned
targetURL, _ := url.Parse("http://example.com/data")
targetURL.Host = safeHost // additional flow step from SafeUrlFlow config
// Flow should propagate to the whole URL after Host assignment
http.Redirect(w, req, targetURL.String(), http.StatusFound) // $ Alert
}
func testHostFieldOverwritten(w http.ResponseWriter, req *http.Request) {
baseURL := req.URL
// Flow should be blocked when Host is overwritten
baseURL.Host = "something.else.com"
http.Get(baseURL.String())
}
func testFieldAccess(w http.ResponseWriter, req *http.Request) {
baseURL := req.URL // $ Source
// Safe field accesses that should preserve flow
host := baseURL.Host
path := baseURL.Path
scheme := baseURL.Scheme
opaquePart := baseURL.Opaque
// Reconstruct URL - flow should be preserved through field access
reconstructed := scheme + "://" + host + path
http.Get(reconstructed) // $ Alert
// Test individual fields
http.Redirect(w, req, "https://"+host+"/path", http.StatusFound) // $ Alert
w.Header().Set("Location", "https://example.com"+path) // $ Alert
http.Post(scheme+"://example.com/api", "application/json", nil) // $ Alert
use(opaquePart) // avoid unused variable warning
// Unsafe field accesses that should be sanitized by UnsafeFieldReadSanitizer
// These read unsafe URL fields and should NOT have flow
unsafeUser := baseURL.User // sanitizer edge (User field)
unsafeQuery := baseURL.RawQuery // sanitizer edge (RawQuery field)
unsafeFragment := baseURL.Fragment // sanitizer edge (Fragment field)
// These should NOT have flow due to sanitizer edges
if unsafeUser != nil {
http.Redirect(w, req, unsafeUser.String(), http.StatusFound) // no flow expected
}
w.Header().Set("Location", "https://example.com/?"+unsafeQuery) // no flow expected
http.Get("https://example.com/#" + unsafeFragment) // no flow expected
}
// Helper function to avoid unused variable warnings
func use(vars ...interface{}) {}

View File

@@ -0,0 +1,15 @@
/**
* @id go/test-safe-url-flow
* @kind path-problem
* @problem.severity recommendation
*/
import go
import semmle.go.security.RequestForgeryCustomizations
import semmle.go.security.OpenUrlRedirectCustomizations
import semmle.go.security.SafeUrlFlow
import SafeUrlFlow::Flow::PathGraph
from SafeUrlFlow::Flow::PathNode source, SafeUrlFlow::Flow::PathNode sink
where SafeUrlFlow::Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "A safe URL flows here from $@.", source.getNode(), "here"

View File

@@ -0,0 +1,4 @@
query: SafeUrlFlow.ql
postprocess:
- utils/test/PrettyPrintModels.ql
- utils/test/InlineExpectationsTestQuery.ql