Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit b6b7bd2

Browse files
committed
Generalise model of HTTP libraries
* Allow for HTTP response methods that define a content-type without a corresponding header write * Factor out stdlib-http-specific classification of fields that aren't vulnerable to an open-redirect exploit
1 parent 6770c74 commit b6b7bd2

File tree

4 files changed

+47
-30
lines changed

4 files changed

+47
-30
lines changed

ql/src/semmle/go/Concepts.qll

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,17 @@ module HTTP {
509509
abstract class Range extends DataFlow::Node {
510510
/** Gets the response writer associated with this header write, if any. */
511511
abstract ResponseWriter getResponseWriter();
512+
513+
/** Gets a content-type associated with this body. */
514+
string getAContentType() { result = getAContentTypeNode().getStringValue() }
515+
516+
/** Gets a dataflow node for a content-type associated with this body. */
517+
DataFlow::Node getAContentTypeNode() {
518+
exists(HTTP::HeaderWrite hw | hw = getResponseWriter().getAHeaderWrite() |
519+
hw.getName().getStringValue().toLowerCase() = "content-type" and
520+
result = hw.getValue()
521+
)
522+
}
512523
}
513524
}
514525

@@ -525,6 +536,12 @@ module HTTP {
525536

526537
/** Gets the response writer associated with this header write, if any. */
527538
ResponseWriter getResponseWriter() { result = self.getResponseWriter() }
539+
540+
/** Gets a content-type associated with this body. */
541+
string getAContentType() { result = self.getAContentType() }
542+
543+
/** Gets a dataflow node for a content-type associated with this body. */
544+
DataFlow::Node getAContentTypeNode() { result = self.getAContentTypeNode() }
528545
}
529546

530547
/** Provides a class for modeling new HTTP client request APIs. */
@@ -576,7 +593,7 @@ module HTTP {
576593
/** Gets the data-flow node representing the URL being redirected to. */
577594
abstract DataFlow::Node getUrl();
578595

579-
/** Gets the response writer that this redirect is sent on. */
596+
/** Gets the response writer that this redirect is sent on, if any. */
580597
abstract ResponseWriter getResponseWriter();
581598
}
582599

@@ -591,6 +608,12 @@ module HTTP {
591608

592609
override ResponseWriter getResponseWriter() { result = HeaderWrite.super.getResponseWriter() }
593610
}
611+
612+
/**
613+
* An HTTP request attribute that is generally not attacker-controllable for
614+
* open redirect exploits; for example, a form field submitted in a POST request.
615+
*/
616+
abstract class UnexploitableSource extends DataFlow::Node { }
594617
}
595618

596619
/**
@@ -607,7 +630,7 @@ module HTTP {
607630
/** Gets the data-flow node representing the URL being redirected to. */
608631
DataFlow::Node getUrl() { result = self.getUrl() }
609632

610-
/** Gets the response writer that this redirect is sent on. */
633+
/** Gets the response writer that this redirect is sent on, if any. */
611634
ResponseWriter getResponseWriter() { result = self.getResponseWriter() }
612635
}
613636
}

ql/src/semmle/go/frameworks/stdlib/NetHttp.qll

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ module NetHttp {
216216
}
217217
}
218218

219+
/** Fields and methods of `net/http.Request` that are not generally exploitable in an open-redirect attack. */
220+
private class RedirectUnexploitableRequestFields extends HTTP::Redirect::UnexploitableSource {
221+
RedirectUnexploitableRequestFields() {
222+
exists(Field f, string fieldName |
223+
f.hasQualifiedName("net/http", "Request", fieldName) and
224+
this = f.getARead()
225+
|
226+
fieldName = ["Body", "GetBody", "PostForm", "MultipartForm", "Header", "Trailer"]
227+
)
228+
or
229+
exists(Method m, string methName |
230+
m.hasQualifiedName("net/http", "Request", methName) and
231+
this = m.getACall()
232+
|
233+
methName = ["Cookie", "Cookies", "MultipartReader", "PostFormValue", "Referer", "UserAgent"]
234+
)
235+
}
236+
}
237+
219238
private class FunctionModels extends TaintTracking::FunctionModel {
220239
FunctionInput inp;
221240
FunctionOutput outp;

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

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,7 @@ module OpenUrlRedirect {
4141
UntrustedFlowAsSource() {
4242
// exclude some fields and methods of URLs that are generally not attacker-controllable for
4343
// open redirect exploits
44-
not exists(Field f, string fieldName |
45-
f.hasQualifiedName("net/http", "Request", fieldName) and
46-
this = f.getARead()
47-
|
48-
fieldName = "Body" or
49-
fieldName = "GetBody" or
50-
fieldName = "PostForm" or
51-
fieldName = "MultipartForm" or
52-
fieldName = "Header" or
53-
fieldName = "Trailer"
54-
) and
55-
not exists(Method m, string methName |
56-
m.hasQualifiedName("net/http", "Request", methName) and
57-
this = m.getACall()
58-
|
59-
methName = "Cookie" or
60-
methName = "Cookies" or
61-
methName = "MultipartReader" or
62-
methName = "PostFormValues" or
63-
methName = "Referer" or
64-
methName = "UserAgent"
65-
)
44+
not this instanceof HTTP::Redirect::UnexploitableSource
6645
}
6746
}
6847

ql/src/semmle/go/security/ReflectedXssCustomizations.qll

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ module ReflectedXss {
4747
* Holds if `body` specifies the response's content type to be HTML.
4848
*/
4949
private predicate htmlTypeSpecified(HTTP::ResponseBody body) {
50-
exists(HTTP::HeaderWrite hw, string tp | hw = body.getResponseWriter().getAHeaderWrite() |
51-
hw.definesHeader("content-type", tp) and tp.regexpMatch("(?i).*html.*")
52-
)
50+
body.getAContentType().regexpMatch("(?i).*html.*")
5351
}
5452

5553
/**
@@ -58,9 +56,7 @@ module ReflectedXss {
5856
private predicate nonHtmlContentType(HTTP::ResponseBody body) {
5957
not htmlTypeSpecified(body) and
6058
(
61-
exists(HTTP::HeaderWrite hw | hw = body.getResponseWriter().getAHeaderWrite() |
62-
hw.getName().getStringValue().toLowerCase() = "content-type"
63-
)
59+
exists(body.getAContentTypeNode())
6460
or
6561
exists(DataFlow::CallNode call | call.getTarget().hasQualifiedName("fmt", "Fprintf") |
6662
body = call.getAnArgument() and

0 commit comments

Comments
 (0)