From a2a95755871b18de3c2d804ccd02db663dc6ced6 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Tue, 30 Sep 2025 11:23:52 +0100 Subject: [PATCH] Add tests for safe URL flow --- .../OpenUrlRedirectCustomizations.qll | 2 +- .../security/RequestForgeryCustomizations.qll | 2 +- go/ql/lib/semmle/go/security/SafeUrlFlow.qll | 6 +- .../security/SafeUrlFlow/SafeUrlFlow.expected | 112 ++++++++++++++ .../go/security/SafeUrlFlow/SafeUrlFlow.go | 139 ++++++++++++++++++ .../go/security/SafeUrlFlow/SafeUrlFlow.ql | 15 ++ .../go/security/SafeUrlFlow/SafeUrlFlow.qlref | 4 + 7 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.expected create mode 100644 go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.go create mode 100644 go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.ql create mode 100644 go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.qlref diff --git a/go/ql/lib/semmle/go/security/OpenUrlRedirectCustomizations.qll b/go/ql/lib/semmle/go/security/OpenUrlRedirectCustomizations.qll index 14c0b232d8b..c278bdf58c5 100644 --- a/go/ql/lib/semmle/go/security/OpenUrlRedirectCustomizations.qll +++ b/go/ql/lib/semmle/go/security/OpenUrlRedirectCustomizations.qll @@ -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 diff --git a/go/ql/lib/semmle/go/security/RequestForgeryCustomizations.qll b/go/ql/lib/semmle/go/security/RequestForgeryCustomizations.qll index a34a47dd7ed..1298785b726 100644 --- a/go/ql/lib/semmle/go/security/RequestForgeryCustomizations.qll +++ b/go/ql/lib/semmle/go/security/RequestForgeryCustomizations.qll @@ -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 diff --git a/go/ql/lib/semmle/go/security/SafeUrlFlow.qll b/go/ql/lib/semmle/go/security/SafeUrlFlow.qll index 77b7aeda591..1fc39072dfb 100644 --- a/go/ql/lib/semmle/go/security/SafeUrlFlow.qll +++ b/go/ql/lib/semmle/go/security/SafeUrlFlow.qll @@ -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 diff --git a/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.expected b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.expected new file mode 100644 index 00000000000..36129fab07c --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.expected @@ -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 diff --git a/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.go b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.go new file mode 100644 index 00000000000..a1844deb6fc --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.go @@ -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{}) {} diff --git a/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.ql b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.ql new file mode 100644 index 00000000000..badc69f386c --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.ql @@ -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" diff --git a/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.qlref b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.qlref new file mode 100644 index 00000000000..db1b80a6317 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/security/SafeUrlFlow/SafeUrlFlow.qlref @@ -0,0 +1,4 @@ +query: SafeUrlFlow.ql +postprocess: + - utils/test/PrettyPrintModels.ql + - utils/test/InlineExpectationsTestQuery.ql