Skip to content

Commit 95d1994

Browse files
committed
Query to check sensitive cookies without the HttpOnly flag set
1 parent cee9677 commit 95d1994

File tree

11 files changed

+922
-0
lines changed

11 files changed

+922
-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 HttpOnly 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: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* @name Sensitive cookies without the HttpOnly response header set
3+
* @description Sensitive cookies without 'HttpOnly' leaves session cookies vulnerable to an XSS attack.
4+
* @kind path-problem
5+
* @id java/sensitive-cookie-not-httponly
6+
* @tags security
7+
* external/cwe/cwe-1004
8+
*/
9+
10+
import java
11+
import semmle.code.java.frameworks.Servlets
12+
import semmle.code.java.dataflow.FlowSources
13+
import semmle.code.java.dataflow.TaintTracking
14+
import DataFlow::PathGraph
15+
16+
/** Gets a regular expression for matching common names of sensitive cookies. */
17+
string getSensitiveCookieNameRegex() { result = "(?i).*(auth|session|token|key|credential).*" }
18+
19+
/** Holds if a string is concatenated with the name of a sensitive cookie. */
20+
predicate isSensitiveCookieNameExpr(Expr expr) {
21+
expr.(StringLiteral)
22+
.getRepresentedString()
23+
.toLowerCase()
24+
.regexpMatch(getSensitiveCookieNameRegex()) or
25+
isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand())
26+
}
27+
28+
/** Holds if a string is concatenated with the `HttpOnly` flag. */
29+
predicate hasHttpOnlyExpr(Expr expr) {
30+
expr.(StringLiteral).getRepresentedString().toLowerCase().matches("%httponly%") or
31+
hasHttpOnlyExpr(expr.(AddExpr).getAnOperand())
32+
}
33+
34+
/** The method call `Set-Cookie` of `addHeader` or `setHeader`. */
35+
class SetCookieMethodAccess extends MethodAccess {
36+
SetCookieMethodAccess() {
37+
(
38+
this.getMethod() instanceof ResponseAddHeaderMethod or
39+
this.getMethod() instanceof ResponseSetHeaderMethod
40+
) and
41+
this.getArgument(0).(StringLiteral).getRepresentedString().toLowerCase() = "set-cookie"
42+
}
43+
}
44+
45+
/** Sensitive cookie name used in a `Cookie` constructor or a `Set-Cookie` call. */
46+
class SensitiveCookieNameExpr extends Expr {
47+
SensitiveCookieNameExpr() {
48+
isSensitiveCookieNameExpr(this) and
49+
(
50+
exists(
51+
ClassInstanceExpr cie // new Cookie("jwt_token", token)
52+
|
53+
(
54+
cie.getConstructor().getDeclaringType().hasQualifiedName("javax.servlet.http", "Cookie") or
55+
cie.getConstructor()
56+
.getDeclaringType()
57+
.getASupertype*()
58+
.hasQualifiedName("javax.ws.rs.core", "Cookie") or
59+
cie.getConstructor()
60+
.getDeclaringType()
61+
.getASupertype*()
62+
.hasQualifiedName("jakarta.ws.rs.core", "Cookie")
63+
) and
64+
DataFlow::localExprFlow(this, cie.getArgument(0))
65+
)
66+
or
67+
exists(
68+
SetCookieMethodAccess ma // response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure")
69+
|
70+
DataFlow::localExprFlow(this, ma.getArgument(1))
71+
)
72+
)
73+
}
74+
}
75+
76+
/** Sink of adding a cookie to the HTTP response. */
77+
class CookieResponseSink extends DataFlow::ExprNode {
78+
CookieResponseSink() {
79+
exists(MethodAccess ma |
80+
(
81+
ma.getMethod() instanceof ResponseAddCookieMethod or
82+
ma instanceof SetCookieMethodAccess
83+
) and
84+
ma.getAnArgument() = this.getExpr()
85+
)
86+
}
87+
}
88+
89+
/** Holds if the `node` is a method call of `setHttpOnly(true)` on a cookie. */
90+
predicate setHttpOnlyMethodAccess(DataFlow::Node node) {
91+
exists(
92+
MethodAccess addCookie, Variable cookie, MethodAccess m // jwtCookie.setHttpOnly(true)
93+
|
94+
addCookie.getMethod() instanceof ResponseAddCookieMethod and
95+
addCookie.getArgument(0) = cookie.getAnAccess() and
96+
m.getMethod().getName() = "setHttpOnly" and
97+
m.getArgument(0).(BooleanLiteral).getBooleanValue() = true and
98+
m.getQualifier() = cookie.getAnAccess() and
99+
node.asExpr() = cookie.getAnAccess()
100+
)
101+
}
102+
103+
/** Holds if the `node` is a method call of `Set-Cookie` header with the `HttpOnly` flag whose cookie name is sensitive. */
104+
predicate setHttpOnlyInSetCookie(DataFlow::Node node) {
105+
exists(SetCookieMethodAccess sa |
106+
hasHttpOnlyExpr(node.asExpr()) and
107+
DataFlow::localExprFlow(node.asExpr(), sa.getArgument(1))
108+
)
109+
}
110+
111+
/** Holds if the `node` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */
112+
predicate setHttpOnlyInNewCookie(DataFlow::Node node) {
113+
exists(ClassInstanceExpr cie |
114+
cie.getConstructor().getDeclaringType().hasName("NewCookie") and
115+
DataFlow::localExprFlow(node.asExpr(), cie.getArgument(0)) and
116+
(
117+
cie.getNumArgument() = 6 and cie.getArgument(5).(BooleanLiteral).getBooleanValue() = true // NewCookie(Cookie cookie, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly)
118+
or
119+
cie.getNumArgument() = 8 and
120+
cie.getArgument(6).getType() instanceof BooleanType and
121+
cie.getArgument(7).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly)
122+
or
123+
cie.getNumArgument() = 10 and cie.getArgument(9).(BooleanLiteral).getBooleanValue() = true // NewCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, Date expiry, boolean secure, boolean httpOnly)
124+
)
125+
)
126+
}
127+
128+
/**
129+
* Holds if the node is a test method indicated by:
130+
* a) in a test directory such as `src/test/java`
131+
* b) in a test package whose name has the word `test`
132+
* c) in a test class whose name has the word `test`
133+
* d) in a test class implementing a test framework such as JUnit or TestNG
134+
*/
135+
predicate isTestMethod(DataFlow::Node node) {
136+
exists(MethodAccess ma, Method m |
137+
node.asExpr() = ma.getAnArgument() and
138+
m = ma.getEnclosingCallable() and
139+
(
140+
m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs
141+
m.getDeclaringType().getPackage().getName().toLowerCase().matches("%test%") or // Simple check to exclude classes in test packages to reduce FPs
142+
exists(m.getLocation().getFile().getAbsolutePath().indexOf("/src/test/java")) or // Match test directory structure of build tools like maven
143+
m instanceof TestMethod // Test method of a test case implementing a test framework such as JUnit or TestNG
144+
)
145+
)
146+
}
147+
148+
/** A taint configuration tracking flow from a sensitive cookie without HttpOnly flag set to its HTTP response. */
149+
class MissingHttpOnlyConfiguration extends TaintTracking::Configuration {
150+
MissingHttpOnlyConfiguration() { this = "MissingHttpOnlyConfiguration" }
151+
152+
override predicate isSource(DataFlow::Node source) {
153+
source.asExpr() instanceof SensitiveCookieNameExpr
154+
}
155+
156+
override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink }
157+
158+
override predicate isSanitizer(DataFlow::Node node) {
159+
// cookie.setHttpOnly(true)
160+
setHttpOnlyMethodAccess(node)
161+
or
162+
// response.addHeader("Set-Cookie: token=" +authId + ";HttpOnly;Secure")
163+
setHttpOnlyInSetCookie(node)
164+
or
165+
// new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true)
166+
setHttpOnlyInNewCookie(node)
167+
or
168+
// Test class or method
169+
isTestMethod(node)
170+
}
171+
172+
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
173+
exists(
174+
ClassInstanceExpr cie // `NewCookie` constructor
175+
|
176+
cie.getAnArgument() = pred.asExpr() and
177+
cie = succ.asExpr() and
178+
cie.getConstructor().getDeclaringType().hasName("NewCookie")
179+
)
180+
or
181+
exists(
182+
MethodAccess ma // `toString` call on a cookie object
183+
|
184+
ma.getQualifier() = pred.asExpr() and
185+
ma.getMethod().hasName("toString") and
186+
ma = succ.asExpr()
187+
)
188+
}
189+
}
190+
191+
from DataFlow::PathNode source, DataFlow::PathNode sink, MissingHttpOnlyConfiguration c
192+
where c.hasFlowPath(source, sink)
193+
select sink.getNode(), source, sink, "$@ doesn't have the HttpOnly flag set.", source.getNode(),
194+
"This sensitive cookie"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
edges
2+
| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie |
3+
| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) |
4+
nodes
5+
| SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | semmle.label | "jwt_token" : String |
6+
| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | semmle.label | jwtCookie |
7+
| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | semmle.label | ... + ... |
8+
| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | semmle.label | toString(...) |
9+
| SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | semmle.label | "session-access-key" : String |
10+
#select
11+
| SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" : String | SensitiveCookieNotHttpOnly.java:28:28:28:36 | jwtCookie | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:22:38:22:48 | "jwt_token" | This sensitive cookie |
12+
| SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:39:42:39:69 | ... + ... | This sensitive cookie |
13+
| SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" : String | SensitiveCookieNotHttpOnly.java:49:42:49:124 | toString(...) | $@ doesn't have the HttpOnly flag set. | SensitiveCookieNotHttpOnly.java:49:56:49:75 | "session-access-key" | This sensitive cookie |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import java.io.IOException;
2+
3+
import javax.servlet.http.Cookie;
4+
import javax.servlet.http.HttpServletRequest;
5+
import javax.servlet.http.HttpServletResponse;
6+
import javax.servlet.ServletException;
7+
8+
import javax.ws.rs.core.NewCookie;
9+
10+
class SensitiveCookieNotHttpOnly {
11+
// GOOD - Tests adding a sensitive cookie with the `HttpOnly` flag set.
12+
public void addCookie(String jwt_token, HttpServletRequest request, HttpServletResponse response) {
13+
Cookie jwtCookie =new Cookie("jwt_token", jwt_token);
14+
jwtCookie.setPath("/");
15+
jwtCookie.setMaxAge(3600*24*7);
16+
jwtCookie.setHttpOnly(true);
17+
response.addCookie(jwtCookie);
18+
}
19+
20+
// BAD - Tests adding a sensitive cookie without the `HttpOnly` flag set.
21+
public void addCookie2(String jwt_token, String userId, HttpServletRequest request, HttpServletResponse response) {
22+
Cookie jwtCookie =new Cookie("jwt_token", jwt_token);
23+
Cookie userIdCookie =new Cookie("user_id", userId.toString());
24+
jwtCookie.setPath("/");
25+
userIdCookie.setPath("/");
26+
jwtCookie.setMaxAge(3600*24*7);
27+
userIdCookie.setMaxAge(3600*24*7);
28+
response.addCookie(jwtCookie);
29+
response.addCookie(userIdCookie);
30+
}
31+
32+
// GOOD - Tests set a sensitive cookie header with the `HttpOnly` flag set.
33+
public void addCookie3(String authId, HttpServletRequest request, HttpServletResponse response) {
34+
response.addHeader("Set-Cookie", "token=" +authId + ";HttpOnly;Secure");
35+
}
36+
37+
// BAD - Tests set a sensitive cookie header without the `HttpOnly` flag set.
38+
public void addCookie4(String authId, HttpServletRequest request, HttpServletResponse response) {
39+
response.addHeader("Set-Cookie", "token=" +authId + ";Secure");
40+
}
41+
42+
// GOOD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through string concatenation.
43+
public void addCookie5(String accessKey, HttpServletRequest request, HttpServletResponse response) {
44+
response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true) + ";HttpOnly");
45+
}
46+
47+
// BAD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` without the `HttpOnly` flag set.
48+
public void addCookie6(String accessKey, HttpServletRequest request, HttpServletResponse response) {
49+
response.setHeader("Set-Cookie", new NewCookie("session-access-key", accessKey, "/", null, null, 0, true).toString());
50+
}
51+
52+
// GOOD - Tests set a sensitive cookie header using the class `javax.ws.rs.core.Cookie` with the `HttpOnly` flag set through the constructor.
53+
public void addCookie7(String accessKey, HttpServletRequest request, HttpServletResponse response) {
54+
NewCookie accessKeyCookie = new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true);
55+
response.setHeader("Set-Cookie", accessKeyCookie.toString());
56+
}
57+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/jsr311-api-1.1.1

0 commit comments

Comments
 (0)