Skip to content

Commit 58d1982

Browse files
authored
Merge pull request github#5663 from smowton/luchua/java/sensitive-cookie-not-httponly
Java: CWE-1004 Query to check sensitive cookies without the HttpOnly flag set w/minor corrections
2 parents 646639b + f22b118 commit 58d1982

File tree

11 files changed

+1041
-24
lines changed

11 files changed

+1041
-24
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: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
* @id java/sensitive-cookie-not-httponly
7+
* @tags security
8+
* external/cwe/cwe-1004
9+
*/
10+
11+
/*
12+
* Sketch of the structure of this query: we track cookie names that appear to be sensitive
13+
* (e.g. `session` or `token`) to a `ServletResponse.addHeader(...)` or `.addCookie(...)`
14+
* method that does not set the `httpOnly` flag. Subsidiary configurations
15+
* `MatchesHttpOnlyConfiguration` and `SetHttpOnlyInCookieConfiguration` are used to establish
16+
* when the `httpOnly` flag is likely to have been set, before configuration
17+
* `MissingHttpOnlyConfiguration` establishes that a non-`httpOnly` cookie has a sensitive-seeming name.
18+
*/
19+
20+
import java
21+
import semmle.code.java.dataflow.FlowSteps
22+
import semmle.code.java.frameworks.Servlets
23+
import semmle.code.java.dataflow.TaintTracking
24+
import semmle.code.java.dataflow.TaintTracking2
25+
import DataFlow::PathGraph
26+
27+
/** Gets a regular expression for matching common names of sensitive cookies. */
28+
string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|credential).*" }
29+
30+
/** Gets a regular expression for matching CSRF cookies. */
31+
string getCsrfCookieNameRegex() { result = "(?i).*(csrf).*" }
32+
33+
/**
34+
* Holds if a string is concatenated with the name of a sensitive cookie. Excludes CSRF cookies since
35+
* they are special cookies implementing the Synchronizer Token Pattern that can be used in JavaScript.
36+
*/
37+
predicate isSensitiveCookieNameExpr(Expr expr) {
38+
exists(string s | s = expr.(CompileTimeConstantExpr).getStringValue() |
39+
s.regexpMatch(getSensitiveCookieNameRegex()) and not s.regexpMatch(getCsrfCookieNameRegex())
40+
)
41+
or
42+
isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand())
43+
}
44+
45+
/** A sensitive cookie name. */
46+
class SensitiveCookieNameExpr extends Expr {
47+
SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) }
48+
}
49+
50+
/** A method call that sets a `Set-Cookie` header. */
51+
class SetCookieMethodAccess extends MethodAccess {
52+
SetCookieMethodAccess() {
53+
(
54+
this.getMethod() instanceof ResponseAddHeaderMethod or
55+
this.getMethod() instanceof ResponseSetHeaderMethod
56+
) and
57+
this.getArgument(0).(CompileTimeConstantExpr).getStringValue().toLowerCase() = "set-cookie"
58+
}
59+
}
60+
61+
/**
62+
* A taint configuration tracking flow from the text `httponly` to argument 1 of
63+
* `SetCookieMethodAccess`.
64+
*/
65+
class MatchesHttpOnlyConfiguration extends TaintTracking2::Configuration {
66+
MatchesHttpOnlyConfiguration() { this = "MatchesHttpOnlyConfiguration" }
67+
68+
override predicate isSource(DataFlow::Node source) {
69+
source.asExpr().(CompileTimeConstantExpr).getStringValue().toLowerCase().matches("%httponly%")
70+
}
71+
72+
override predicate isSink(DataFlow::Node sink) {
73+
sink.asExpr() = any(SetCookieMethodAccess ma).getArgument(1)
74+
}
75+
}
76+
77+
/** A class descended from `javax.servlet.http.Cookie` or `javax/jakarta.ws.rs.core.Cookie`. */
78+
class CookieClass extends RefType {
79+
CookieClass() {
80+
this.getASupertype*()
81+
.hasQualifiedName(["javax.servlet.http", "javax.ws.rs.core", "jakarta.ws.rs.core"], "Cookie")
82+
}
83+
}
84+
85+
/** Holds if `expr` is any boolean-typed expression other than literal `false`. */
86+
// Inlined because this could be a very large result set if computed out of context
87+
pragma[inline]
88+
predicate mayBeBooleanTrue(Expr expr) {
89+
expr.getType() instanceof BooleanType and
90+
not expr.(CompileTimeConstantExpr).getBooleanValue() = false
91+
}
92+
93+
/** Holds if the method call may set the `HttpOnly` flag. */
94+
predicate setsCookieHttpOnly(MethodAccess ma) {
95+
ma.getMethod().getName() = "setHttpOnly" and
96+
// any use of setHttpOnly(x) where x isn't false is probably safe
97+
mayBeBooleanTrue(ma.getArgument(0))
98+
}
99+
100+
/** Holds if `ma` removes a cookie. */
101+
predicate removesCookie(MethodAccess ma) {
102+
ma.getMethod().getName() = "setMaxAge" and
103+
ma.getArgument(0).(IntegerLiteral).getIntValue() = 0
104+
}
105+
106+
/**
107+
* Holds if the MethodAccess `ma` is a test method call indicated by:
108+
* a) in a test directory such as `src/test/java`
109+
* b) in a test package whose name has the word `test`
110+
* c) in a test class whose name has the word `test`
111+
* d) in a test class implementing a test framework such as JUnit or TestNG
112+
*/
113+
predicate isTestMethod(MethodAccess ma) {
114+
exists(Method m |
115+
m = ma.getEnclosingCallable() and
116+
(
117+
m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs
118+
m.getDeclaringType().getPackage().getName().toLowerCase().matches("%test%") or // Simple check to exclude classes in test packages to reduce FPs
119+
exists(m.getLocation().getFile().getAbsolutePath().indexOf("/src/test/java")) or // Match test directory structure of build tools like maven
120+
m instanceof TestMethod // Test method of a test case implementing a test framework such as JUnit or TestNG
121+
)
122+
)
123+
}
124+
125+
/**
126+
* A taint configuration tracking flow of a method that sets the `HttpOnly` flag,
127+
* or one that removes a cookie, to a `ServletResponse.addCookie` call.
128+
*/
129+
class SetHttpOnlyOrRemovesCookieConfiguration extends TaintTracking2::Configuration {
130+
SetHttpOnlyOrRemovesCookieConfiguration() { this = "SetHttpOnlyOrRemovesCookieConfiguration" }
131+
132+
override predicate isSource(DataFlow::Node source) {
133+
source.asExpr() =
134+
any(MethodAccess ma | setsCookieHttpOnly(ma) or removesCookie(ma)).getQualifier()
135+
}
136+
137+
override predicate isSink(DataFlow::Node sink) {
138+
sink.asExpr() =
139+
any(MethodAccess ma | ma.getMethod() instanceof ResponseAddCookieMethod).getArgument(0)
140+
}
141+
}
142+
143+
/**
144+
* A cookie that is added to an HTTP response and which doesn't have `httpOnly` set, used as a sink
145+
* in `MissingHttpOnlyConfiguration`.
146+
*/
147+
class CookieResponseSink extends DataFlow::ExprNode {
148+
CookieResponseSink() {
149+
exists(MethodAccess ma |
150+
(
151+
ma.getMethod() instanceof ResponseAddCookieMethod and
152+
this.getExpr() = ma.getArgument(0) and
153+
not exists(SetHttpOnlyOrRemovesCookieConfiguration cc | cc.hasFlowTo(this))
154+
or
155+
ma instanceof SetCookieMethodAccess and
156+
this.getExpr() = ma.getArgument(1) and
157+
not exists(MatchesHttpOnlyConfiguration cc | cc.hasFlowTo(this)) // response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure")
158+
) and
159+
not isTestMethod(ma) // Test class or method
160+
)
161+
}
162+
}
163+
164+
/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */
165+
predicate setsHttpOnlyInNewCookie(ClassInstanceExpr cie) {
166+
cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and
167+
(
168+
cie.getNumArgument() = 6 and
169+
mayBeBooleanTrue(cie.getArgument(5)) // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly)
170+
or
171+
cie.getNumArgument() = 8 and
172+
cie.getArgument(6).getType() instanceof BooleanType and
173+
mayBeBooleanTrue(cie.getArgument(7)) // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly)
174+
or
175+
cie.getNumArgument() = 10 and
176+
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)
177+
)
178+
}
179+
180+
/**
181+
* A taint configuration tracking flow from a sensitive cookie without the `HttpOnly` flag
182+
* set to its HTTP response.
183+
*/
184+
class MissingHttpOnlyConfiguration extends TaintTracking::Configuration {
185+
MissingHttpOnlyConfiguration() { this = "MissingHttpOnlyConfiguration" }
186+
187+
override predicate isSource(DataFlow::Node source) {
188+
source.asExpr() instanceof SensitiveCookieNameExpr
189+
}
190+
191+
override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink }
192+
193+
override predicate isSanitizer(DataFlow::Node node) {
194+
// JAX-RS's `new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true)` and similar
195+
setsHttpOnlyInNewCookie(node.asExpr())
196+
}
197+
198+
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
199+
exists(
200+
ConstructorCall cc // new Cookie(...)
201+
|
202+
cc.getConstructedType() instanceof CookieClass and
203+
pred.asExpr() = cc.getAnArgument() and
204+
succ.asExpr() = cc
205+
)
206+
or
207+
exists(
208+
MethodAccess ma // cookie.toString()
209+
|
210+
ma.getMethod().getName() = "toString" and
211+
ma.getQualifier().getType() instanceof CookieClass and
212+
pred.asExpr() = ma.getQualifier() and
213+
succ.asExpr() = ma
214+
)
215+
}
216+
}
217+
218+
from DataFlow::PathNode source, DataFlow::PathNode sink, MissingHttpOnlyConfiguration c
219+
where c.hasFlowPath(source, sink)
220+
select sink.getNode(), source, sink, "$@ doesn't have the HttpOnly flag set.", source.getNode(),
221+
"This sensitive cookie"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
edges
2+
| SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie |
3+
| SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... |
4+
| SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... |
5+
| SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) |
6+
| SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr |
7+
| SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString |
8+
| SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString |
9+
| SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString |
10+
| SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie |
11+
| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie |
12+
| SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie | SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie |
13+
nodes
14+
| SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | semmle.label | "jwt_token" : String |
15+
| SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | semmle.label | jwtCookie |
16+
| SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | semmle.label | "token=" : String |
17+
| SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | semmle.label | ... + ... : String |
18+
| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | semmle.label | ... + ... |
19+
| SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | semmle.label | toString(...) |
20+
| SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | semmle.label | "session-access-key" : String |
21+
| SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | semmle.label | "session-access-key" : String |
22+
| SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | semmle.label | keyStr |
23+
| SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | semmle.label | "token=" : String |
24+
| SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | semmle.label | ... + ... : String |
25+
| SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | semmle.label | ... + ... : String |
26+
| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | semmle.label | secString |
27+
| SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | semmle.label | "Presto-UI-Token" : String |
28+
| SensitiveCookieNotHttpOnly.java:91:16:91:21 | cookie : Cookie | semmle.label | cookie : Cookie |
29+
| SensitiveCookieNotHttpOnly.java:110:25:110:64 | createAuthenticationCookie(...) : Cookie | semmle.label | createAuthenticationCookie(...) : Cookie |
30+
| SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | semmle.label | cookie |
31+
#select
32+
| SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:31:28:31:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:24:33:24:43 | "jwt_token" | This sensitive cookie |
33+
| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:49 | "token=" | This sensitive cookie |
34+
| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... : String | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:57 | ... + ... | This sensitive cookie |
35+
| SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:42:42:42:69 | ... + ... | This sensitive cookie |
36+
| SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:52:42:52:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:52:56:52:75 | "session-access-key" | This sensitive cookie |
37+
| SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:65:42:65:47 | keyStr | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:63:51:63:70 | "session-access-key" | This sensitive cookie |
38+
| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:35 | "token=" | This sensitive cookie |
39+
| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:43 | ... + ... | This sensitive cookie |
40+
| SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... : String | SensitiveCookieNotHttpOnly.java:71:42:71:50 | secString | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:70:28:70:55 | ... + ... | This sensitive cookie |
41+
| SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" : String | SensitiveCookieNotHttpOnly.java:111:28:111:33 | cookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:88:35:88:51 | "Presto-UI-Token" | This sensitive cookie |

0 commit comments

Comments
 (0)