Skip to content

Commit 04b0682

Browse files
committed
Use isAdditionalTaintStep and make the query more readable
1 parent 11304b2 commit 04b0682

File tree

1 file changed

+75
-71
lines changed

1 file changed

+75
-71
lines changed

java/ql/src/experimental/Security/CWE/CWE-1004/SensitiveCookieNotHttpOnly.ql

Lines changed: 75 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
/**
22
* @name Sensitive cookies without the HttpOnly response header set
33
* @description Sensitive cookies without the 'HttpOnly' flag set leaves session cookies vulnerable to
4-
* an XSS attack. This query checks whether 'HttpOnly' is not set in a Java Cookie object
5-
* or the 'Set-Cookie' HTTP header.
4+
* an XSS attack.
65
* @kind path-problem
76
* @id java/sensitive-cookie-not-httponly
87
* @tags security
98
* external/cwe/cwe-1004
109
*/
1110

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+
1220
import java
1321
import semmle.code.java.dataflow.FlowSteps
1422
import semmle.code.java.frameworks.Servlets
@@ -34,6 +42,11 @@ predicate isSensitiveCookieNameExpr(Expr expr) {
3442
isSensitiveCookieNameExpr(expr.(AddExpr).getAnOperand())
3543
}
3644

45+
/** A sensitive cookie name. */
46+
class SensitiveCookieNameExpr extends Expr {
47+
SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) }
48+
}
49+
3750
/** A method call that sets a `Set-Cookie` header. */
3851
class SetCookieMethodAccess extends MethodAccess {
3952
SetCookieMethodAccess() {
@@ -70,36 +83,55 @@ class CookieClass extends RefType {
7083
}
7184

7285
/** Holds if the `expr` is `true` or a variable that is ever assigned `true`. */
86+
// This could be a very large result set if computed out of context
87+
pragma[inline]
7388
predicate mayBeBooleanTrue(Expr expr) {
74-
expr.(CompileTimeConstantExpr).getBooleanValue() = true or
75-
expr.(VarAccess).getVariable().getAnAssignedValue().(CompileTimeConstantExpr).getBooleanValue() =
76-
true
89+
expr.getType() instanceof BooleanType and
90+
not expr.(CompileTimeConstantExpr).getBooleanValue() = false
7791
}
7892

79-
/** Holds if the method call sets the `HttpOnly` flag. */
93+
/** Holds if the method call may set the `HttpOnly` flag. */
8094
predicate setsCookieHttpOnly(MethodAccess ma) {
8195
ma.getMethod().getName() = "setHttpOnly" and
82-
(
83-
ma.getArgument(0).(CompileTimeConstantExpr).getBooleanValue() = true
84-
or
85-
// any use of setHttpOnly(x) where x isn't false is probably safe
86-
not exists(VarAccess va |
87-
ma.getArgument(0).(VarAccess).getVariable().getAnAccess() = va and
88-
va.getVariable().getAnAssignedValue().(CompileTimeConstantExpr).getBooleanValue() = false
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
89121
)
90122
)
91123
}
92124

93125
/**
94-
* A taint configuration tracking flow of a method or a wrapper method that sets
95-
* the `HttpOnly` flag.
126+
* A taint configuration tracking flow of a method or a wrapper method that sets the `HttpOnly`
127+
* flag, or one that removes a cookie, to a `ServletResponse.addCookie` call.
96128
*/
97129
class SetHttpOnlyInCookieConfiguration extends TaintTracking2::Configuration {
98130
SetHttpOnlyInCookieConfiguration() { this = "SetHttpOnlyInCookieConfiguration" }
99131

100132
override predicate isSource(DataFlow::Node source) {
101133
source.asExpr() =
102-
any(MethodAccess ma | setsCookieHttpOnly(ma) or removeCookie(ma)).getQualifier()
134+
any(MethodAccess ma | setsCookieHttpOnly(ma) or removesCookie(ma)).getQualifier()
103135
}
104136

105137
override predicate isSink(DataFlow::Node sink) {
@@ -108,18 +140,10 @@ class SetHttpOnlyInCookieConfiguration extends TaintTracking2::Configuration {
108140
}
109141
}
110142

111-
/** Holds if `ma` removes a cookie. */
112-
predicate removeCookie(MethodAccess ma) {
113-
ma.getMethod().getName() = "setMaxAge" and
114-
ma.getArgument(0).(IntegerLiteral).getIntValue() = 0
115-
}
116-
117-
/** A sensitive cookie name. */
118-
class SensitiveCookieNameExpr extends Expr {
119-
SensitiveCookieNameExpr() { isSensitiveCookieNameExpr(this) }
120-
}
121-
122-
/** A cookie that is added to an HTTP response, used as a sink in `MissingHttpOnlyConfiguration`. */
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+
*/
123147
class CookieResponseSink extends DataFlow::ExprNode {
124148
CookieResponseSink() {
125149
exists(MethodAccess ma |
@@ -137,11 +161,8 @@ class CookieResponseSink extends DataFlow::ExprNode {
137161
}
138162
}
139163

140-
/**
141-
* Holds if `ClassInstanceExpr` cie is an invocation of a JAX-RS `NewCookie` constructor
142-
* that sets `HttpOnly` to true.
143-
*/
144-
predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) {
164+
/** Holds if `cie` is an invocation of a JAX-RS `NewCookie` constructor that sets `HttpOnly` to true. */
165+
predicate setsHttpOnlyInNewCookie(ClassInstanceExpr cie) {
145166
cie.getConstructedType().hasQualifiedName(["javax.ws.rs.core", "jakarta.ws.rs.core"], "NewCookie") and
146167
(
147168
cie.getNumArgument() = 6 and
@@ -156,42 +177,6 @@ predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) {
156177
)
157178
}
158179

159-
/** The cookie constructor. */
160-
class CookieTaintPreservingConstructor extends Constructor, TaintPreservingCallable {
161-
CookieTaintPreservingConstructor() { this.getDeclaringType() instanceof CookieClass }
162-
163-
override predicate returnsTaintFrom(int arg) { arg = 0 }
164-
}
165-
166-
/** The method call `toString` to get a stringified cookie representation. */
167-
class CookieToString extends TaintPreservingCallable {
168-
CookieToString() {
169-
this.getDeclaringType() instanceof CookieClass and
170-
this.hasName("toString")
171-
}
172-
173-
override predicate returnsTaintFrom(int arg) { arg = -1 }
174-
}
175-
176-
/**
177-
* Holds if the MethodAccess `ma` is a test method call indicated by:
178-
* a) in a test directory such as `src/test/java`
179-
* b) in a test package whose name has the word `test`
180-
* c) in a test class whose name has the word `test`
181-
* d) in a test class implementing a test framework such as JUnit or TestNG
182-
*/
183-
predicate isTestMethod(MethodAccess ma) {
184-
exists(Method m |
185-
m = ma.getEnclosingCallable() and
186-
(
187-
m.getDeclaringType().getName().toLowerCase().matches("%test%") or // Simple check to exclude test classes to reduce FPs
188-
m.getDeclaringType().getPackage().getName().toLowerCase().matches("%test%") or // Simple check to exclude classes in test packages to reduce FPs
189-
exists(m.getLocation().getFile().getAbsolutePath().indexOf("/src/test/java")) or // Match test directory structure of build tools like maven
190-
m instanceof TestMethod // Test method of a test case implementing a test framework such as JUnit or TestNG
191-
)
192-
)
193-
}
194-
195180
/**
196181
* A taint configuration tracking flow from a sensitive cookie without the `HttpOnly` flag
197182
* set to its HTTP response.
@@ -206,8 +191,27 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration {
206191
override predicate isSink(DataFlow::Node sink) { sink instanceof CookieResponseSink }
207192

208193
override predicate isSanitizer(DataFlow::Node node) {
209-
// new NewCookie("session-access-key", accessKey, "/", null, null, 0, true, true)
210-
setHttpOnlyInNewCookie(node.asExpr())
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+
)
211215
}
212216
}
213217

0 commit comments

Comments
 (0)