Skip to content

Commit 937049e

Browse files
authored
Merge pull request #891 from xiemaisi/js/simplify-sensitive-actions
Approved by esben-semmle
2 parents 90eccbd + aebc5bc commit 937049e

File tree

13 files changed

+158
-116
lines changed

13 files changed

+158
-116
lines changed

javascript/ql/src/Security/CWE-312/CleartextStorage.qhelp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ causing logged sensitive information to be stored as well.
3535

3636
<example>
3737
<p>
38-
The following example code stores user credentials (in this case, their account
39-
name) in a cookie in plain text:
38+
The following example code stores user credentials (in this case, their password) in a cookie in plain text:
4039
</p>
4140
<sample src="examples/CleartextStorage.js"/>
4241
<p>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
var express = require('express');
22

33
var app = express();
4-
app.get('/', function (req, res) {
5-
let accountName = req.param("AccountName");
4+
app.get('/remember-password', function (req, res) {
5+
let pw = req.param("current_password");
66
// BAD: Setting a cookie value with cleartext sensitive data.
7-
res.cookie("AccountName", accountName);
7+
res.cookie("password", pw);
88
});

javascript/ql/src/Security/CWE-312/examples/CleartextStorageGood.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ function encrypt(text){
88
}
99

1010
var app = express();
11-
app.get('/', function (req, res) {
12-
let accountName = req.param("AccountName");
11+
app.get('/remember-password', function (req, res) {
12+
let pw = req.param("current_password");
1313
// GOOD: Encoding the value before setting it.
14-
res.cookie("AccountName", encrypt(accountName));
14+
res.cookie("password", encrypt(pw));
1515
});

javascript/ql/src/semmle/javascript/security/SensitiveActions.qll

Lines changed: 110 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,56 @@ import javascript
1717
* INTERNAL: Do not use directly.
1818
*/
1919
module HeuristicNames {
20-
/** Gets a regular expression that identifies strings that look like they represent secret data that are not passwords. */
21-
string suspiciousNonPassword() { result = "(?is).*(secret|account|accnt|(?<!un)trusted).*" }
20+
/**
21+
* Gets a regular expression that identifies strings that may indicate the presence of secret
22+
* or trusted data.
23+
*/
24+
string maybeSecret() { result = "(?is).*((?<!is)secret|(?<!un|is)trusted).*" }
2225

23-
/** Gets a regular expression that identifies strings that look like they represent secret data that are passwords. */
24-
string suspiciousPassword() { result = "(?is).*(password|passwd).*" }
26+
/**
27+
* Gets a regular expression that identifies strings that may indicate the presence of
28+
* user names or other account information.
29+
*/
30+
string maybeAccountInfo() {
31+
result = "(?is).*acc(ou)?nt.*" or
32+
result = "(?is).*(puid|username|userid).*"
33+
}
2534

26-
/** Gets a regular expression that identifies strings that look like they represent secret data. */
27-
string suspicious() { result = suspiciousPassword() or result = suspiciousNonPassword() }
35+
/**
36+
* Gets a regular expression that identifies strings that may indicate the presence of
37+
* a password or an authorization key.
38+
*/
39+
string maybePassword() {
40+
result = "(?is).*pass(wd|word|code|phrase)(?!.*question).*" or
41+
result = "(?is).*(auth(entication|ori[sz]ation)?)key.*"
42+
}
2843

2944
/**
30-
* Gets a regular expression that identifies strings that look like they represent data that is
31-
* hashed or encrypted.
45+
* Gets a regular expression that identifies strings that may indicate the presence of
46+
* a certificate.
3247
*/
33-
string nonSuspicious() {
34-
result = "(?is).*(redact|censor|obfuscate|hash|md5|sha|((?<!un)(en))?(crypt|code)).*"
48+
string maybeCertificate() { result = "(?is).*(cert)(?!.*(format|name)).*" }
49+
50+
/**
51+
* Gets a regular expression that identifies strings that may indicate the presence
52+
* of sensitive data, with `classification` describing the kind of sensitive data involved.
53+
*/
54+
string maybeSensitive(SensitiveExpr::Classification classification) {
55+
result = maybeSecret() and classification = SensitiveExpr::secret()
56+
or
57+
result = maybeAccountInfo() and classification = SensitiveExpr::id()
58+
or
59+
result = maybePassword() and classification = SensitiveExpr::password()
60+
or
61+
result = maybeCertificate() and classification = SensitiveExpr::certificate()
3562
}
3663

3764
/**
38-
* Gets a regular expression that identifies names that look like they represent credential information.
65+
* Gets a regular expression that identifies strings that may indicate the presence of data
66+
* that is hashed or encrypted, and hence rendered non-sensitive.
3967
*/
40-
string suspiciousCredentials() {
41-
result = "(?i).*pass(wd|word|code|phrase)(?!.*question).*" or
42-
result = "(?i).*(puid|username|userid).*" or
43-
result = "(?i).*(cert)(?!.*(format|name)).*" or
44-
result = "(?i).*(auth(entication|ori[sz]ation)?)key.*"
68+
string notSensitive() {
69+
result = "(?is).*(redact|censor|obfuscate|hash|md5|sha|((?<!un)(en))?(crypt|code)).*"
4570
}
4671
}
4772
private import HeuristicNames
@@ -50,22 +75,60 @@ private import HeuristicNames
5075
abstract class SensitiveExpr extends Expr {
5176
/** Gets a human-readable description of this expression for use in alert messages. */
5277
abstract string describe();
78+
79+
/** Gets a classification of the kind of sensitive data this expression might contain. */
80+
abstract SensitiveExpr::Classification getClassification();
81+
}
82+
83+
module SensitiveExpr {
84+
/**
85+
* A classification of different kinds of sensitive data:
86+
*
87+
* - secret: generic secret or trusted data;
88+
* - id: a user name or other account information;
89+
* - password: a password or authorization key;
90+
* - certificate: a certificate.
91+
*
92+
* While classifications are represented as strings, this should not be relied upon.
93+
* Instead, use the predicates below to work with classifications.
94+
*/
95+
class Classification extends string {
96+
Classification() {
97+
this = "secret" or this = "id" or this = "password" or this = "certificate"
98+
}
99+
}
100+
101+
/** Gets the classification for secret or trusted data. */
102+
Classification secret() { result = "secret" }
103+
104+
/** Gets the classification for user names or other account information. */
105+
Classification id() { result = "id" }
106+
107+
/** Gets the classification for passwords or authorization keys. */
108+
Classification password() { result = "password" }
109+
110+
/** Gets the classification for certificates. */
111+
Classification certificate() { result = "certificate" }
53112
}
54113

55114
/** A function call that might produce sensitive data. */
56115
class SensitiveCall extends SensitiveExpr, InvokeExpr {
116+
SensitiveExpr::Classification classification;
117+
57118
SensitiveCall() {
58-
this.getCalleeName() instanceof SensitiveDataFunctionName
119+
classification = this.getCalleeName().(SensitiveDataFunctionName).getClassification()
59120
or
60121
// This is particularly to pick up methods with an argument like "password", which
61122
// may indicate a lookup.
62123
exists(string s | this.getAnArgument().mayHaveStringValue(s) |
63-
s.regexpMatch(suspicious()) and
64-
not s.regexpMatch(nonSuspicious())
124+
s.regexpMatch(maybeSensitive(classification)) and
125+
not s.regexpMatch(notSensitive())
65126
)
66127
}
67128

68129
override string describe() { result = "a call to " + getCalleeName() }
130+
131+
override SensitiveExpr::Classification getClassification() { result = classification }
69132
}
70133

71134
/** An access to a variable or property that might contain sensitive data. */
@@ -85,14 +148,17 @@ abstract class SensitiveVariableAccess extends SensitiveExpr {
85148
}
86149

87150
/** A write to a location that might contain sensitive data. */
88-
abstract class SensitiveWrite extends DataFlow::Node { }
151+
abstract class SensitiveWrite extends DataFlow::Node {
152+
}
89153

90154
/** A write to a variable or property that might contain sensitive data. */
91155
private class BasicSensitiveWrite extends SensitiveWrite {
156+
SensitiveExpr::Classification classification;
157+
92158
BasicSensitiveWrite() {
93159
exists(string name |
94-
name.regexpMatch(suspicious()) and
95-
not name.regexpMatch(nonSuspicious())
160+
name.regexpMatch(maybeSensitive(classification)) and
161+
not name.regexpMatch(notSensitive())
96162
|
97163
exists(DataFlow::PropWrite pwn |
98164
pwn.getPropertyName() = name and
@@ -110,13 +176,20 @@ private class BasicSensitiveWrite extends SensitiveWrite {
110176
)
111177
)
112178
}
179+
180+
/** Gets a classification of the kind of sensitive data the write might handle. */
181+
SensitiveExpr::Classification getClassification() { result = classification }
113182
}
114183

115184
/** An access to a variable or property that might contain sensitive data. */
116185
private class BasicSensitiveVariableAccess extends SensitiveVariableAccess {
186+
SensitiveExpr::Classification classification;
187+
117188
BasicSensitiveVariableAccess() {
118-
name.regexpMatch(suspicious()) and not name.regexpMatch(nonSuspicious())
189+
name.regexpMatch(maybeSensitive(classification)) and not name.regexpMatch(notSensitive())
119190
}
191+
192+
override SensitiveExpr::Classification getClassification() { result = classification }
120193
}
121194

122195
/** A function name that suggests it may be sensitive. */
@@ -129,11 +202,18 @@ abstract class SensitiveFunctionName extends string {
129202
}
130203

131204
/** A function name that suggests it may produce sensitive data. */
132-
abstract class SensitiveDataFunctionName extends SensitiveFunctionName { }
205+
abstract class SensitiveDataFunctionName extends SensitiveFunctionName {
206+
/** Gets a classification of the kind of sensitive data this function may produce. */
207+
abstract SensitiveExpr::Classification getClassification();
208+
}
133209

134210
/** A method that might return sensitive data, based on the name. */
135211
class CredentialsFunctionName extends SensitiveDataFunctionName {
136-
CredentialsFunctionName() { this.regexpMatch(suspicious()) }
212+
SensitiveExpr::Classification classification;
213+
214+
CredentialsFunctionName() { this.regexpMatch(maybeSensitive(classification)) }
215+
216+
override SensitiveExpr::Classification getClassification() { result = classification }
137217
}
138218

139219
/**
@@ -163,41 +243,10 @@ class ProtectCall extends DataFlow::CallNode {
163243
}
164244
}
165245

166-
/**
167-
* Classes for expressions containing cleartext passwords.
168-
*/
169-
private module CleartextPasswords {
170-
bindingset[name]
171-
private predicate isCleartextPasswordIndicator(string name) {
172-
name.regexpMatch(suspiciousPassword()) and
173-
not name.regexpMatch(nonSuspicious())
174-
}
175-
176-
/** An expression that might contain a cleartext password. */
177-
abstract class CleartextPasswordExpr extends SensitiveExpr { }
178-
179-
/** A function name that suggests it may produce a cleartext password. */
180-
private class CleartextPasswordDataFunctionName extends SensitiveDataFunctionName {
181-
CleartextPasswordDataFunctionName() { isCleartextPasswordIndicator(this) }
182-
}
246+
/** An expression that might contain a clear-text password. */
247+
class CleartextPasswordExpr extends SensitiveExpr {
248+
CleartextPasswordExpr() { this.(SensitiveExpr).getClassification() = SensitiveExpr::password() }
183249

184-
/** A call that might return a cleartext password. */
185-
private class CleartextPasswordCallExpr extends CleartextPasswordExpr, SensitiveCall {
186-
CleartextPasswordCallExpr() {
187-
this.getCalleeName() instanceof CleartextPasswordDataFunctionName
188-
or
189-
// This is particularly to pick up methods with an argument like "password", which
190-
// may indicate a lookup.
191-
exists(string s |
192-
this.getAnArgument().mayHaveStringValue(s) and
193-
isCleartextPasswordIndicator(s)
194-
)
195-
}
196-
}
197-
198-
/** An access to a variable or property that might contain a cleartext password. */
199-
private class CleartextPasswordLookupExpr extends CleartextPasswordExpr, SensitiveVariableAccess {
200-
CleartextPasswordLookupExpr() { isCleartextPasswordIndicator(name) }
201-
}
250+
override string describe() { none() }
251+
override SensitiveExpr::Classification getClassification() { none() }
202252
}
203-
import CleartextPasswords

javascript/ql/src/semmle/javascript/security/dataflow/CleartextLogging.qll

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ module CleartextLogging {
7070
*/
7171
private class NameGuidedNonCleartextPassword extends NonCleartextPassword {
7272
NameGuidedNonCleartextPassword() {
73-
exists(string name | name.regexpMatch(nonSuspicious()) |
73+
exists(string name | name.regexpMatch(notSensitive()) |
7474
this.asExpr().(VarAccess).getName() = name
7575
or
7676
this.(DataFlow::PropRead).getPropertyName() = name
@@ -111,7 +111,7 @@ module CleartextLogging {
111111
* A call that might obfuscate a password, for example through hashing.
112112
*/
113113
private class ObfuscatorCall extends Barrier, DataFlow::InvokeNode {
114-
ObfuscatorCall() { getCalleeName().regexpMatch(nonSuspicious()) }
114+
ObfuscatorCall() { getCalleeName().regexpMatch(notSensitive()) }
115115
}
116116

117117
/**
@@ -129,8 +129,8 @@ module CleartextLogging {
129129

130130
ObjectPasswordPropertySource() {
131131
exists(DataFlow::PropWrite write |
132-
name.regexpMatch(suspiciousPassword()) and
133-
not name.regexpMatch(nonSuspicious()) and
132+
name.regexpMatch(maybePassword()) and
133+
not name.regexpMatch(notSensitive()) and
134134
write = this.(DataFlow::SourceNode).getAPropertyWrite(name) and
135135
// avoid safe values assigned to presumably unsafe names
136136
not write.getRhs() instanceof NonCleartextPassword
@@ -147,7 +147,7 @@ module CleartextLogging {
147147
ReadPasswordSource() {
148148
// avoid safe values assigned to presumably unsafe names
149149
not this instanceof NonCleartextPassword and
150-
name.regexpMatch(suspiciousPassword()) and
150+
name.regexpMatch(maybePassword()) and
151151
(
152152
this.asExpr().(VarAccess).getName() = name
153153
or

javascript/ql/src/semmle/javascript/security/dataflow/CleartextStorage.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ module CleartextStorage {
5050
class SensitiveExprSource extends Source, DataFlow::ValueNode {
5151
override SensitiveExpr astNode;
5252

53+
SensitiveExprSource() {
54+
// storing user names or account names in plaintext isn't usually a problem
55+
astNode.getClassification() != SensitiveExpr::id()
56+
}
57+
5358
override string describe() { result = astNode.describe() }
5459
}
5560

javascript/ql/src/semmle/javascript/security/dataflow/InsufficientPasswordHash.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ module InsufficientPasswordHash {
5252
* with insufficient computational effort.
5353
*/
5454
class CleartextPasswordSource extends Source, DataFlow::ValueNode {
55-
override CleartextPasswordExpr astNode;
55+
override SensitiveExpr astNode;
56+
57+
CleartextPasswordSource() { astNode.getClassification() = SensitiveExpr::password() }
5658

5759
override string describe() { result = astNode.describe() }
5860
}

javascript/ql/src/semmle/javascript/security/dataflow/PostMessageStar.qll

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import javascript
7-
private import semmle.javascript.security.SensitiveActions
7+
private import semmle.javascript.security.SensitiveActions::HeuristicNames
88

99
module PostMessageStar {
1010
/**
@@ -70,7 +70,7 @@ module PostMessageStar {
7070
)
7171
or
7272
// `toString` or `JSON.toString` on a partially tainted object gives a tainted value
73-
exists (DataFlow::InvokeNode toString | toString = trg |
73+
exists(DataFlow::InvokeNode toString | toString = trg |
7474
toString.(DataFlow::MethodCallNode).calls(src, "toString")
7575
or
7676
toString = DataFlow::globalVarRef("JSON").getAMemberCall("stringify") and
@@ -92,23 +92,6 @@ module PostMessageStar {
9292
*/
9393
class SensitiveExprSource extends Source, DataFlow::ValueNode { override SensitiveExpr astNode; }
9494

95-
/**
96-
* A variable/property access or function call whose name suggests that it may contain credentials,
97-
* viewed as a data flow source for cross-window communication with unrestricted origin.
98-
*/
99-
class CredentialsSource extends Source {
100-
CredentialsSource() {
101-
exists(string name |
102-
name = this.(DataFlow::InvokeNode).getCalleeName() or
103-
name = this.(DataFlow::PropRead).getPropertyName() or
104-
name = this.asExpr().(VarUse).getVariable().getName()
105-
|
106-
name.regexpMatch(HeuristicNames::suspiciousCredentials()) and
107-
not name.regexpMatch(HeuristicNames::nonSuspicious())
108-
)
109-
}
110-
}
111-
11295
/** A call to any function whose name suggests that it encodes or encrypts its arguments. */
11396
class ProtectSanitizer extends Sanitizer { ProtectSanitizer() { this instanceof ProtectCall } }
11497

0 commit comments

Comments
 (0)