Skip to content

Commit 54aefe0

Browse files
Copy experimental query to main
1 parent b57243e commit 54aefe0

File tree

3 files changed

+294
-0
lines changed

3 files changed

+294
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
class SensitiveCookieNotHttpOnly {
2+
// GOOD - Create a sensitive cookie with the `HttpOnly` flag set.
3+
public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) {
4+
Cookie jwtCookie =new Cookie("jwt_token", jwt_token);
5+
jwtCookie.setPath("/");
6+
jwtCookie.setMaxAge(3600*24*7);
7+
jwtCookie.setHttpOnly(true);
8+
response.addCookie(jwtCookie);
9+
}
10+
11+
// BAD - Create a sensitive cookie without the `HttpOnly` flag set.
12+
public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) {
13+
Cookie jwtCookie =new Cookie("jwt_token", jwt_token);
14+
jwtCookie.setPath("/");
15+
jwtCookie.setMaxAge(3600*24*7);
16+
response.addCookie(jwtCookie);
17+
}
18+
19+
// GOOD - Set a sensitive cookie header with the `HttpOnly` flag set.
20+
public void addCookie3(String authId, HttpServletRequest request, HttpServletResponse response) {
21+
response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure");
22+
}
23+
24+
// BAD - Set a sensitive cookie header without the `HttpOnly` flag set.
25+
public void addCookie4(String authId, HttpServletRequest request, HttpServletResponse response) {
26+
response.addHeader("Set-Cookie", "token=" +authId + ";Secure");
27+
}
28+
29+
// GOOD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through string concatenation.
30+
public void addCookie5(String accessKey, HttpServletRequest request, HttpServletResponse response) {
31+
response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true) + ";HttpOnly");
32+
}
33+
34+
// BAD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` without the `HttpOnly` flag set.
35+
public void addCookie6(String accessKey, HttpServletRequest request, HttpServletResponse response) {
36+
response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true).toString());
37+
}
38+
39+
// GOOD - Set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through the constructor.
40+
public void addCookie7(String accessKey, HttpServletRequest request, HttpServletResponse response) {
41+
NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true);
42+
response.setHeader("Set-Cookie", accessKeyCookie.toString());
43+
}
44+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>Cross-Site Scripting (XSS) is categorized as one of the OWASP Top 10 Security Vulnerabilities. The <code>HttpOnly</code> flag directs compatible browsers to prevent client-side script from accessing cookies. Including the <code>HttpOnly</code> flag in the Set-Cookie HTTP response header for a sensitive cookie helps mitigate the risk associated with XSS where an attacker's script code attempts to read the contents of a cookie and exfiltrate information obtained.</p>
6+
</overview>
7+
8+
<recommendation>
9+
<p>Use the <code>HttpOnly</code> flag when generating a cookie containing sensitive information to help mitigate the risk of client side script accessing the protected cookie.</p>
10+
</recommendation>
11+
12+
<example>
13+
<p>The following example shows two ways of generating sensitive cookies. In the 'BAD' cases, the <code>HttpOnly</code> flag is not set. In the 'GOOD' cases, the <code>HttpOnly</code> flag is set.</p>
14+
<sample src="SensitiveCookieNotHttpOnly.java" />
15+
</example>
16+
17+
<references>
18+
<li>
19+
PortSwigger:
20+
<a href="https://portswigger.net/kb/issues/00500600_cookie-without-httponly-flag-set">Cookie without HttpOnly flag set</a>
21+
</li>
22+
<li>
23+
OWASP:
24+
<a href="https://owasp.org/www-community/HttpOnly">HttpOnly</a>
25+
</li>
26+
</references>
27+
</qhelp>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/**
2+
* @name Sensitive cookies without the HttpOnly response header set
3+
* @description Sensitive cookies without the 'HttpOnly' flag set leaves session cookies vulnerable to
4+
* an XSS attack.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision medium
8+
* @id java/sensitive-cookie-not-httponly
9+
* @tags security
10+
* external/cwe/cwe-1004
11+
*/
12+
13+
/*
14+
* Sketch of the structure of this query: we track cookie names that appear to be sensitive
15+
* (e.g. `session` or `token`) to a `ServletResponse.addHeader(...)` or `.addCookie(...)`
16+
* method that does not set the `httpOnly` flag. Subsidiary configurations
17+
* `MatchesHttpOnlyConfiguration` and `SetHttpOnlyInCookieConfiguration` are used to establish
18+
* when the `httpOnly` flag is likely to have been set, before configuration
19+
* `MissingHttpOnlyConfiguration` establishes that a non-`httpOnly` cookie has a sensitive-seeming name.
20+
*/
21+
22+
import java
23+
import semmle.code.java.dataflow.FlowSteps
24+
import semmle.code.java.frameworks.Servlets
25+
import semmle.code.java.dataflow.TaintTracking
26+
import MissingHttpOnlyFlow::PathGraph
27+
28+
/** Gets a regular expression for matching common names of sensitive cookies. */
29+
string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|credential).*" }
30+
31+
/** Gets a regular expression for matching CSRF cookies. */
32+
string getCsrfCookieNameRegex() { result = "(?i).*(csrf).*" }
33+
34+
/**
35+
* Holds if a string is concatenated with the name of a sensitive cookie. Excludes CSRF cookies since
36+
* they are special cookies implementing the Synchronizer Token Pattern that can be used in JavaScript.
37+
*/
38+
predicate isSensitiveCookieNameExpr(Expr expr) {
39+
exists(string s | s = expr.(CompileTimeConstantExpr).getStringValue() |
40+
s.regexpMatch(getSensitiveCookieNameRegex()) and not s.regexpMatch(getCsrfCookieNameRegex())
41+
)
42+
or
43+
isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand())
44+
}
45+
46+
/** A sensitive cookie name. */
47+
class SensitiveCookieNameExpr extends Expr {
48+
SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) }
49+
}
50+
51+
/** A method call that sets a `Set-Cookie` header. */
52+
class SetCookieMethodCall extends MethodCall {
53+
SetCookieMethodCall() {
54+
(
55+
this.getMethod() instanceof ResponseAddHeaderMethod or
56+
this.getMethod() instanceof ResponseSetHeaderMethod
57+
) and
58+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue().toLowerCase() = "set-cookie"
59+
}
60+
}
61+
62+
/**
63+
* A taint configuration tracking flow from the text `httponly` to argument 1 of
64+
* `SetCookieMethodCall`.
65+
*/
66+
module MatchesHttpOnlyConfig implements DataFlow::ConfigSig {
67+
predicate isSource(DataFlow::Node source) {
68+
source.asExpr().(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%")
69+
}
70+
71+
predicate isSink(DataFlow::Node sink) {
72+
sink.asExpr() = any(SetCookieMethodCall ma).getArgument(1)
73+
}
74+
}
75+
76+
module MatchesHttpOnlyFlow = TaintTracking::Global<MatchesHttpOnlyConfig>;
77+
78+
/** A class descended from `javax.servlet.http.Cookie`. */
79+
class CookieClass extends RefType {
80+
CookieClass() { this.getAnAncestor().hasQualifiedName("javax.servlet.http", "Cookie") }
81+
}
82+
83+
/** Holds if `expr` is any boolean-typed expression other than literal `false`. */
84+
// Inlined because this could be a very large result set if computed out of context
85+
pragma[inline]
86+
predicate mayBeBooleanTrue(Expr expr) {
87+
expr.getType() instanceof BooleanType and
88+
not expr.(CompileTimeConstantExpr).getBooleanValue() = false
89+
}
90+
91+
/** Holds if the method call may set the `HttpOnly` flag. */
92+
predicate setsCookieHttpOnly(MethodCall ma) {
93+
ma.getMethod().getName() = "setHttpOnly" and
94+
// any use of setHttpOnly(x) where x isn't false is probably safe
95+
mayBeBooleanTrue(ma.getArgument(0))
96+
}
97+
98+
/** Holds if `ma` removes a cookie. */
99+
predicate removesCookie(MethodCall ma) {
100+
ma.getMethod().getName() = "setMaxAge" and
101+
ma.getArgument(0).(IntegerLiteral).getIntValue() = 0
102+
}
103+
104+
/**
105+
* Holds if the MethodCall `ma` is a test method call indicated by:
106+
* a) in a test directory such as `src/test/java`
107+
* b) in a test package whose name has the word `test`
108+
* c) in a test class whose name has the word `test`
109+
* d) in a test class implementing a test framework such as JUnit or TestNG
110+
*/
111+
predicate isTestMethod(MethodCall ma) {
112+
exists(Method m |
113+
m = ma.getEnclosingCallable() and
114+
(
115+
m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs
116+
m.getDeclaringType().getPackage().getName().toLowerCase().matches("%test%") or // Simple check to exclude classes in test packages to reduce FPs
117+
exists(m.getLocation().getFile().getAbsolutePath().indexOf("/src/test/java")) or // Match test directory structure of build tools like maven
118+
m instanceof TestMethod // Test method of a test case implementing a test framework such as JUnit or TestNG
119+
)
120+
)
121+
}
122+
123+
/**
124+
* A taint configuration tracking flow of a method that sets the `HttpOnly` flag,
125+
* or one that removes a cookie, to a `ServletResponse.addCookie` call.
126+
*/
127+
module SetHttpOnlyOrRemovesCookieConfig implements DataFlow::ConfigSig {
128+
predicate isSource(DataFlow::Node source) {
129+
source.asExpr() =
130+
any(MethodCall ma | setsCookieHttpOnly(ma) or removesCookie(ma)).getQualifier()
131+
}
132+
133+
predicate isSink(DataFlow::Node sink) {
134+
sink.asExpr() =
135+
any(MethodCall ma | ma.getMethod() instanceof ResponseAddCookieMethod).getArgument(0)
136+
}
137+
}
138+
139+
module SetHttpOnlyOrRemovesCookieFlow = TaintTracking::Global<SetHttpOnlyOrRemovesCookieConfig>;
140+
141+
/**
142+
* A cookie that is added to an HTTP response and which doesn't have `httpOnly` set, used as a sink
143+
* in `MissingHttpOnlyConfiguration`.
144+
*/
145+
class CookieResponseSink extends DataFlow::ExprNode {
146+
CookieResponseSink() {
147+
exists(MethodCall ma |
148+
(
149+
ma.getMethod() instanceof ResponseAddCookieMethod and
150+
this.getExpr() = ma.getArgument(0) and
151+
not SetHttpOnlyOrRemovesCookieFlow::flowTo(this)
152+
or
153+
ma instanceof SetCookieMethodCall and
154+
this.getExpr() = ma.getArgument(1) and
155+
not MatchesHttpOnlyFlow::flowTo(this) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure")
156+
) and
157+
not isTestMethod(ma) // Test class or method
158+
)
159+
}
160+
}
161+
162+
/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */
163+
predicate setsHttpOnlyInNewCookie(ClassInstanceExpr cie) {
164+
cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and
165+
(
166+
cie.getNumArgument() = 6 and
167+
mayBeBooleanTrue(cie.getArgument(5)) // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly)
168+
or
169+
cie.getNumArgument() = 8 and
170+
cie.getArgument(6).getType() instanceof BooleanType and
171+
mayBeBooleanTrue(cie.getArgument(7)) // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly)
172+
or
173+
cie.getNumArgument() = 10 and
174+
mayBeBooleanTrue(cie.getArgument(9)) // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly)
175+
)
176+
}
177+
178+
/**
179+
* A taint configuration tracking flow from a sensitive cookie without the `HttpOnly` flag
180+
* set to its HTTP response.
181+
*/
182+
module MissingHttpOnlyConfig implements DataFlow::ConfigSig {
183+
predicate isSource(DataFlow::Node source) { source.asExpr() instanceof SensitiveCookieNameExpr }
184+
185+
predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink }
186+
187+
predicate isBarrier(DataFlow::Node node) {
188+
// JAX-RS's `new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true)` and similar
189+
setsHttpOnlyInNewCookie(node.asExpr())
190+
}
191+
192+
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
193+
exists(
194+
ConstructorCall cc // new Cookie(...)
195+
|
196+
cc.getConstructedType() instanceof CookieClass and
197+
pred.asExpr() = cc.getAnArgument() and
198+
succ.asExpr() = cc
199+
)
200+
or
201+
exists(
202+
MethodCall ma // cookie.toString()
203+
|
204+
ma.getMethod().getName() = "toString" and
205+
ma.getQualifier().getType() instanceof CookieClass and
206+
pred.asExpr() = ma.getQualifier() and
207+
succ.asExpr() = ma
208+
)
209+
}
210+
}
211+
212+
module MissingHttpOnlyFlow = TaintTracking::Global<MissingHttpOnlyConfig>;
213+
214+
deprecated query predicate problems(
215+
DataFlow::Node sinkNode, MissingHttpOnlyFlow::PathNode source, MissingHttpOnlyFlow::PathNode sink,
216+
string message1, DataFlow::Node sourceNode, string message2
217+
) {
218+
MissingHttpOnlyFlow::flowPath(source, sink) and
219+
sinkNode = sink.getNode() and
220+
message1 = "$@ doesn't have the HttpOnly flag set." and
221+
sourceNode = source.getNode() and
222+
message2 = "This sensitive cookie"
223+
}

0 commit comments

Comments
 (0)