Skip to content

Commit a2bc1b5

Browse files
authored
Merge pull request #1 from npesaresi/feature/SSRF
CWE-918
2 parents e39753c + 210d0f3 commit a2bc1b5

File tree

18 files changed

+1899
-0
lines changed

18 files changed

+1899
-0
lines changed

ql/src/experimental/CWE-918/SSRF.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
func handler(w http.ResponseWriter, req *http.Request) {
8+
target := req.FormValue("target")
9+
10+
// BAD: `target` is controlled by the attacker
11+
resp, err := http.Get("https://example.com/current_api/" + target)
12+
if err != nil {
13+
// error handling
14+
}
15+
16+
// process request response
17+
use(resp)
18+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Directly incorporating user input into an HTTP request without validating the input can facilitate
9+
server side request forgery attacks, where the attacker essentially controls the request.
10+
</p>
11+
</overview>
12+
13+
<recommendation>
14+
<p>
15+
To guard against server side request forgery, it is advisable to avoid putting user input directly into a
16+
network request. If using user input is necessary, then is mandatory to validate them. Only allow numeric and alphanumeric values.
17+
URL encoding is not a solution in certain scenarios, such as, an architecture build over NGINX proxies.
18+
</p>
19+
</recommendation>
20+
21+
<example>
22+
<p>
23+
The following example shows an HTTP request parameter being used directly in a URL request without
24+
validating the input, which facilitates an SSRF attack. The request <code>http.Get("https://example.com/current_api/"+target)</code> is
25+
vulnerable since attackers can choose the value of <code>target</code> to be anything they want. For
26+
instance, the attacker can choose <code>"../super_secret_api"</code> as the target, causing the
27+
URL to become <code>"https://example.com/super_secret_api"</code>.
28+
</p>
29+
30+
<p>
31+
A request to <code>https://example.com/super_secret_api</code> may be problematic if that api is not
32+
meant to be directly accessible from the attacker's machine.
33+
</p>
34+
35+
<sample src="SSRF.go"/>
36+
37+
<p>
38+
One way to remedy the problem is to validate the user input to only allow alphanumeric values:
39+
</p>
40+
41+
<sample src="SSRFGood.go"/>
42+
</example>
43+
44+
<references>
45+
46+
<li>OWASP: <a href="https://www.owasp.org/www-community/attacks/Server_Side_Request_Forgery">SSRF</a></li>
47+
48+
</references>
49+
</qhelp>

ql/src/experimental/CWE-918/SSRF.ql

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name Uncontrolled data used in network request
3+
* @description Sending network requests with user-controlled data allows for request forgery attacks.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @precision high
7+
* @tags security
8+
* external/cwe/cwe-918
9+
*/
10+
11+
import go
12+
import SSRF.SSRF
13+
import DataFlow::PathGraph
14+
15+
from
16+
SSRF::Configuration cfg, DataFlow::PathNode source,
17+
DataFlow::PathNode sink, DataFlow::Node request
18+
where
19+
cfg.hasFlowPath(source, sink) and
20+
request = sink.getNode().(SSRF::Sink).getARequest()
21+
select request, source, sink, "The URL of this request depends on a user-provided value"

ql/src/experimental/CWE-918/SSRF.qll

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* Provides a taint-tracking configuration for reasoning about request forgery
3+
* (SSRF) vulnerabilities.
4+
*/
5+
6+
import go
7+
8+
/**
9+
* Provides a taint-tracking configuration for reasoning about request forgery
10+
* (SSRF) vulnerabilities.
11+
*/
12+
module SSRF {
13+
import semmle.go.frameworks.Gin
14+
import SSRF.validator
15+
import semmle.go.security.UrlConcatenation
16+
import semmle.go.dataflow.barrierguardutil.RegexpCheck
17+
import semmle.go.dataflow.Properties
18+
19+
//#region config
20+
/**
21+
* A taint-tracking configuration for reasoning about request forgery.
22+
*/
23+
class Configuration extends TaintTracking::Configuration {
24+
Configuration() { this = "SSRF" }
25+
26+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
27+
28+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
29+
30+
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
31+
// propagate to a URL when its host is assigned to
32+
exists(Write w, Field f, SsaWithFields v | f.hasQualifiedName("net/url", "URL", "Host") |
33+
w.writesField(v.getAUse(), f, pred) and succ = v.getAUse()
34+
)
35+
}
36+
37+
override predicate isSanitizer(DataFlow::Node node) {
38+
super.isSanitizer(node) or
39+
node instanceof Sanitizer
40+
}
41+
42+
override predicate isSanitizerOut(DataFlow::Node node) {
43+
super.isSanitizerOut(node) or
44+
node instanceof SanitizerEdge
45+
}
46+
47+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
48+
super.isSanitizerGuard(guard) or guard instanceof SanitizerGuard
49+
}
50+
}
51+
52+
//#endregion
53+
//#region abstract classes
54+
/** A data flow source for request forgery vulnerabilities. */
55+
abstract class Source extends DataFlow::Node { }
56+
57+
/** A data flow sink for request forgery vulnerabilities. */
58+
abstract class Sink extends DataFlow::Node {
59+
/** Gets a request that uses this sink. */
60+
abstract DataFlow::Node getARequest();
61+
62+
/**
63+
* Gets the name of a part of the request that may be tainted by this sink,
64+
* such as the URL or the host.
65+
*/
66+
abstract string getKind();
67+
}
68+
69+
/** A sanitizer for request forgery vulnerabilities. */
70+
abstract class Sanitizer extends DataFlow::Node { }
71+
72+
/** An outgoing sanitizer edge for request forgery vulnerabilities. */
73+
abstract class SanitizerEdge extends DataFlow::Node { }
74+
75+
/**
76+
* A sanitizer guard for request forgery vulnerabilities.
77+
*/
78+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
79+
80+
//#endregion
81+
//#region source
82+
/**
83+
* An user controlled input, considered as a flow source for request forgery.
84+
*/
85+
class UntrustedFlowAsSource extends Source, UntrustedFlowSource { }
86+
87+
//#endregion
88+
//#region sink
89+
/**
90+
* The URL of an HTTP request, viewed as a sink for request forgery.
91+
*/
92+
private class ClientRequestUrlAsSink extends Sink {
93+
HTTP::ClientRequest request;
94+
95+
ClientRequestUrlAsSink() { this = request.getUrl() }
96+
97+
override DataFlow::Node getARequest() { result = request }
98+
99+
override string getKind() { result = "URL" }
100+
}
101+
102+
/**
103+
* The URL of a WebSocket request, viewed as a sink for request forgery.
104+
*/
105+
class WebSocketCallAsSink extends Sink {
106+
WebSocketRequestCall request;
107+
108+
WebSocketCallAsSink() { this = request.getRequestUrl() }
109+
110+
override DataFlow::Node getARequest() { result = request }
111+
112+
override string getKind() { result = "WebSocket URL" }
113+
}
114+
115+
//#endregion
116+
//#region sanitizers
117+
/**
118+
* Result value of prepending a string that prevents any value from controlling the
119+
* host of a URL.
120+
*/
121+
private class PathSanitizer extends SanitizerEdge {
122+
PathSanitizer() { sanitizingPrefixEdge(this, _) }
123+
}
124+
125+
/**
126+
* A call to a regexp match function, considered as a barrier guard for sanitizing untrusted URLs.
127+
*
128+
* This is overapproximate: we do not attempt to reason about the correctness of the regexp.
129+
*/
130+
class RegexpCheckAsBarrierGuard extends RegexpCheck, SanitizerGuard { }
131+
132+
/**
133+
* An equality check comparing a data-flow node against a constant string, considered as
134+
* a barrier guard for sanitizing untrusted URLs.
135+
*/
136+
class EqualityAsSanitizerGuard extends SanitizerGuard, DataFlow::EqualityTestNode {
137+
DataFlow::Node url;
138+
139+
EqualityAsSanitizerGuard() {
140+
exists(this.getAnOperand().getStringValue()) and
141+
url = this.getAnOperand()
142+
}
143+
144+
override predicate checks(Expr e, boolean outcome) {
145+
e = url.asExpr() and outcome = this.getPolarity()
146+
}
147+
}
148+
149+
/**
150+
* If the tainted variable is a boolean or has numeric type is not possible to exploit a SSRF
151+
*/
152+
class NumSanitizer extends Sanitizer {
153+
NumSanitizer() {
154+
this.getType() instanceof NumericType or
155+
this.getType() instanceof BoolType
156+
}
157+
}
158+
159+
/**
160+
* When we receive a body from a request, we can use certain tags on our struct's fields to hint
161+
* the binding function to run some validations for that field. If these binding functions returns
162+
* no error, then we consider these fields safe for SSRF.
163+
*/
164+
class BodySanitizer extends Sanitizer, BodyTagSanitizer {}
165+
166+
/**
167+
* The method Var of package validator is a sanitizer guard only if the check
168+
* of the error binding exists, and the tag to check is one of "alpha", "alphanum", "alphaunicode", "alphanumunicode", "number", "numeric".
169+
*/
170+
class ValidatorAsSanitizer extends SanitizerGuard, ValidatorVarCheck {}
171+
//#endregion
172+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"github.com/go-playground/validator"
6+
)
7+
8+
func handler(w http.ResponseWriter, req *http.Request) {
9+
validate := validator.New()
10+
target := req.FormValue("target")
11+
if validate.Var(target, "alphanum")
12+
// GOOD: `target` is alphanumeric
13+
resp, err := http.Get("https://example.com/current_api/" + target)
14+
if err != nil {
15+
// error handling
16+
}
17+
18+
// process request response
19+
use(resp)
20+
}

0 commit comments

Comments
 (0)