1
1
/**
2
2
* @name Sensitive cookies without the HttpOnly response header set
3
3
* @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.
6
5
* @kind path-problem
7
6
* @id java/sensitive-cookie-not-httponly
8
7
* @tags security
9
8
* external/cwe/cwe-1004
10
9
*/
11
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
+
12
20
import java
13
21
import semmle.code.java.dataflow.FlowSteps
14
22
import semmle.code.java.frameworks.Servlets
@@ -34,6 +42,11 @@ predicate isSensitiveCookieNameExpr(Expr expr) {
34
42
isSensitiveCookieNameExpr ( expr .( AddExpr ) .getAnOperand ( ) )
35
43
}
36
44
45
+ /** A sensitive cookie name. */
46
+ class SensitiveCookieNameExpr extends Expr {
47
+ SensitiveCookieNameExpr ( ) { isSensitiveCookieNameExpr ( this ) }
48
+ }
49
+
37
50
/** A method call that sets a `Set-Cookie` header. */
38
51
class SetCookieMethodAccess extends MethodAccess {
39
52
SetCookieMethodAccess ( ) {
@@ -70,36 +83,55 @@ class CookieClass extends RefType {
70
83
}
71
84
72
85
/** 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]
73
88
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
77
91
}
78
92
79
- /** Holds if the method call sets the `HttpOnly` flag. */
93
+ /** Holds if the method call may set the `HttpOnly` flag. */
80
94
predicate setsCookieHttpOnly ( MethodAccess ma ) {
81
95
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
89
121
)
90
122
)
91
123
}
92
124
93
125
/**
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 .
96
128
*/
97
129
class SetHttpOnlyInCookieConfiguration extends TaintTracking2:: Configuration {
98
130
SetHttpOnlyInCookieConfiguration ( ) { this = "SetHttpOnlyInCookieConfiguration" }
99
131
100
132
override predicate isSource ( DataFlow:: Node source ) {
101
133
source .asExpr ( ) =
102
- any ( MethodAccess ma | setsCookieHttpOnly ( ma ) or removeCookie ( ma ) ) .getQualifier ( )
134
+ any ( MethodAccess ma | setsCookieHttpOnly ( ma ) or removesCookie ( ma ) ) .getQualifier ( )
103
135
}
104
136
105
137
override predicate isSink ( DataFlow:: Node sink ) {
@@ -108,18 +140,10 @@ class SetHttpOnlyInCookieConfiguration extends TaintTracking2::Configuration {
108
140
}
109
141
}
110
142
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
+ */
123
147
class CookieResponseSink extends DataFlow:: ExprNode {
124
148
CookieResponseSink ( ) {
125
149
exists ( MethodAccess ma |
@@ -137,11 +161,8 @@ class CookieResponseSink extends DataFlow::ExprNode {
137
161
}
138
162
}
139
163
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 ) {
145
166
cie .getConstructedType ( ) .hasQualifiedName ( [ "javax.ws.rs.core" , "jakarta.ws.rs.core" ] , "NewCookie" ) and
146
167
(
147
168
cie .getNumArgument ( ) = 6 and
@@ -156,42 +177,6 @@ predicate setHttpOnlyInNewCookie(ClassInstanceExpr cie) {
156
177
)
157
178
}
158
179
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
-
195
180
/**
196
181
* A taint configuration tracking flow from a sensitive cookie without the `HttpOnly` flag
197
182
* set to its HTTP response.
@@ -206,8 +191,27 @@ class MissingHttpOnlyConfiguration extends TaintTracking::Configuration {
206
191
override predicate isSink ( DataFlow:: Node sink ) { sink instanceof CookieResponseSink }
207
192
208
193
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
+ )
211
215
}
212
216
}
213
217
0 commit comments