Skip to content

Commit 0c4fb15

Browse files
author
Esben Sparre Andreasen
committed
JS: add query js/cleartext-logging
1 parent b4952e7 commit 0c4fb15

File tree

19 files changed

+469
-0
lines changed

19 files changed

+469
-0
lines changed

change-notes/1.18/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
| **Query** | **Tags** | **Purpose** |
3030
|-----------------------------|-----------|--------------------------------------------------------------------|
31+
| Clear text logging of sensitive information (`js/cleartext-logging`) | security, external/cwe/cwe-312, external/cwe/cwe-315, external/cwe/cwe-359 | Highlights logging of sensitive information, indicating a violation of [CWE-312](https://cwe.mitre.org/data/definitions/312.html). Results shown on lgtm by default. |
3132
| Disabling Electron webSecurity (`js/disabling-electron-websecurity`) | security, frameworks/electron | Highlights Electron browser objects that are created with the `webSecurity` property set to false. Results shown on LGTM by default. |
3233
| Enabling Electron allowRunningInsecureContent (`js/enabling-electron-insecure-content`) | security, frameworks/electron | Highlights Electron browser objects that are created with the `allowRunningInsecureContent` property set to true. Results shown on LGTM by default. |
3334
| Use of externally-controlled format string (`js/tainted-format-string`) | security, external/cwe/cwe-134 | Highlights format strings containing user-provided data, indicating a violation of [CWE-134](https://cwe.mitre.org/data/definitions/134.html). Results shown on LGTM by default. |

javascript/config/suites/javascript/security

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
+ semmlecode-javascript-queries/Security/CWE-134/TaintedFormatString.ql: /Security/CWE/CWE-134
1010
+ semmlecode-javascript-queries/Security/CWE-209/StackTraceExposure.ql: /Security/CWE/CWE-209
1111
+ semmlecode-javascript-queries/Security/CWE-312/CleartextStorage.ql: /Security/CWE/CWE-312
12+
+ semmlecode-javascript-queries/Security/CWE-312/CleartextLogging.ql: /Security/CWE/CWE-312
1213
+ semmlecode-javascript-queries/Security/CWE-313/PasswordInConfigurationFile.ql: /Security/CWE/CWE-313
1314
+ semmlecode-javascript-queries/Security/CWE-327/BrokenCryptoAlgorithm.ql: /Security/CWE/CWE-327
1415
+ semmlecode-javascript-queries/Security/CWE-338/InsecureRandomness.ql: /Security/CWE/CWE-338
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<include src="CleartextStorage.qhelp" /></qhelp>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @name Clear text logging of sensitive information
3+
* @description Sensitive information logged without encryption or hashing can expose it to an
4+
* attacker.
5+
* @kind problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id js/cleartext-logging
9+
* @tags security
10+
* external/cwe/cwe-312
11+
* external/cwe/cwe-315
12+
* external/cwe/cwe-359
13+
*/
14+
15+
import javascript
16+
import semmle.javascript.security.dataflow.CleartextLogging::CleartextLogging
17+
18+
/**
19+
* Holds if `tl` is used in a browser environment.
20+
*/
21+
predicate inBrowserEnvironment(TopLevel tl) {
22+
tl instanceof InlineScript or
23+
tl instanceof CodeInAttribute or
24+
exists (GlobalVarAccess e |
25+
e.getTopLevel() = tl |
26+
e.getName() = "window"
27+
) or
28+
exists (Module m | inBrowserEnvironment(m) |
29+
tl = m.getAnImportedModule() or
30+
m = tl.(Module).getAnImportedModule()
31+
)
32+
}
33+
34+
/**
35+
* Holds if `sink` only is reachable in a "test" environment.
36+
*/
37+
predicate inTestEnvironment(Sink sink) {
38+
exists (IfStmt guard, Identifier id |
39+
// heuristic: a deliberate environment choice by the programmer related to passwords implies a test environment
40+
id.getName().regexpMatch("(?i).*(test|develop|production).*") and
41+
id.(Expr).getParentExpr*() = guard.getCondition() and
42+
(
43+
guard.getAControlledStmt() = sink.asExpr().getEnclosingStmt() or
44+
guard.getAControlledStmt().(BlockStmt).getAChildStmt() = sink.asExpr().getEnclosingStmt()
45+
)
46+
)
47+
}
48+
49+
from Configuration cfg, Source source, DataFlow::Node sink
50+
where cfg.hasFlow(source, sink) and
51+
// ignore logging to the browser console (even though it is not a good practice)
52+
not inBrowserEnvironment(sink.asExpr().getTopLevel()) and
53+
// ignore logging when testing
54+
not inTestEnvironment(sink)
55+
select sink, "Sensitive data returned by $@ is logged here.", source, source.describe()

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ sensitive information.
2222
In general, decrypt sensitive information only at the point where it is
2323
necessary for it to be used in cleartext.
2424
</p>
25+
26+
<p>
27+
28+
Be aware that external processes often store the <code>standard
29+
out</code> and <code>standard error</code> streams of the application,
30+
causing logged sensitive information to be stored as well.
31+
32+
</p>
33+
2534
</recommendation>
2635

2736
<example>
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/**
2+
* Provides a dataflow tracking configuration for reasoning about cleartext logging of sensitive information.
3+
*/
4+
import javascript
5+
private import semmle.javascript.dataflow.InferredTypes
6+
private import semmle.javascript.security.SensitiveActions::HeuristicNames
7+
8+
module CleartextLogging {
9+
/**
10+
* A data flow source for cleartext logging of sensitive information.
11+
*/
12+
abstract class Source extends DataFlow::Node {
13+
/** Gets a string that describes the type of this data flow source. */
14+
abstract string describe();
15+
}
16+
17+
/**
18+
* A data flow sink for cleartext logging of sensitive information.
19+
*/
20+
abstract class Sink extends DataFlow::Node { }
21+
22+
/**
23+
* A barrier for cleartext logging of sensitive information.
24+
*/
25+
abstract class Barrier extends DataFlow::Node { }
26+
27+
/**
28+
* A dataflow tracking configuration for cleartext logging of sensitive information.
29+
*
30+
* This configuration identifies flows from `Source`s, which are sources of
31+
* sensitive data, to `Sink`s, which is an abstract class representing all
32+
* the places sensitive data may be stored in cleartext. Additional sources or sinks can be
33+
* added either by extending the relevant class, or by subclassing this configuration itself,
34+
* and amending the sources and sinks.
35+
*/
36+
class Configuration extends DataFlow::Configuration {
37+
Configuration() { this = "CleartextLogging" }
38+
39+
override
40+
predicate isSource(DataFlow::Node source) {
41+
source instanceof Source
42+
}
43+
44+
override
45+
predicate isSink(DataFlow::Node sink) {
46+
sink instanceof Sink
47+
}
48+
49+
override
50+
predicate isBarrier(DataFlow::Node node) {
51+
node instanceof Barrier
52+
}
53+
54+
override predicate isAdditionalFlowStep(DataFlow::Node src, DataFlow::Node trg) {
55+
// A taint propagating data flow edge arising from string operations
56+
exists (AST::ValueNode astNode |
57+
astNode = trg.(DataFlow::ValueNode).getAstNode() |
58+
// addition propagates
59+
astNode.(AddExpr).getAnOperand() = src.asExpr() or
60+
astNode.(AssignAddExpr).getAChildExpr() = src.asExpr() or
61+
exists (SsaExplicitDefinition ssa |
62+
astNode = ssa.getVariable().getAUse() and
63+
src.asExpr().(AssignAddExpr) = ssa.getDef()
64+
)
65+
or
66+
// templating propagates
67+
astNode.(TemplateLiteral).getAnElement() = src.asExpr()
68+
or
69+
// other string operations that propagate
70+
exists (string name | name = astNode.(MethodCallExpr).getMethodName() |
71+
src.asExpr() = astNode.(MethodCallExpr).getReceiver() and
72+
(
73+
// sorted, interesting, properties of Object.prototype
74+
name = "toString" or
75+
name = "valueOf"
76+
)
77+
)
78+
)
79+
or
80+
// A taint propagating data flow edge through objects: a tainted write taints the entire object.
81+
exists (DataFlow::PropWrite write |
82+
write.getRhs() = src and
83+
trg.(DataFlow::SourceNode).flowsTo(write.getBase())
84+
)
85+
}
86+
}
87+
88+
/**
89+
* An argument to a logging mechanism.
90+
*/
91+
class LoggerSink extends Sink {
92+
LoggerSink() {
93+
this = any(LoggerCall log).getAMessageComponent()
94+
}
95+
}
96+
97+
/**
98+
* A data flow node that does not contain a clear text password, according to its syntactic name.
99+
*/
100+
private class NameGuidedNonCleartextPassword extends NonCleartextPassword {
101+
102+
NameGuidedNonCleartextPassword() {
103+
exists (string name |
104+
name.regexpMatch(nonSuspicious()) |
105+
this.asExpr().(VarAccess).getName() = name
106+
or
107+
this.(DataFlow::PropRead).getPropertyName() = name
108+
or
109+
this.(DataFlow::InvokeNode).getCalleeName() = name
110+
)
111+
or
112+
// avoid i18n strings
113+
this.(DataFlow::PropRead).getBase().asExpr().(VarRef).getName().regexpMatch("(?is).*(messages|strings).*")
114+
}
115+
116+
}
117+
118+
/**
119+
* A data flow node that is definitely not an object.
120+
*/
121+
private class NonObject extends NonCleartextPassword {
122+
123+
NonObject() {
124+
forall (AbstractValue v | v = analyze().getAValue() |
125+
not v.getType() = TTObject()
126+
)
127+
}
128+
129+
}
130+
131+
/**
132+
* A data flow node that receives flow that is not a clear text password.
133+
*/
134+
private class NonCleartextPasswordFlow extends NonCleartextPassword {
135+
136+
NonCleartextPasswordFlow() {
137+
any(NonCleartextPassword other).(DataFlow::SourceNode).flowsTo(this)
138+
}
139+
140+
}
141+
142+
/**
143+
* A call that might obfuscate a password, for example through hashing.
144+
*/
145+
private class ObfuscatorCall extends Barrier, DataFlow::InvokeNode {
146+
147+
ObfuscatorCall() {
148+
getCalleeName().regexpMatch(nonSuspicious())
149+
}
150+
151+
}
152+
153+
/**
154+
* A data flow node that does not contain a clear text password.
155+
*/
156+
private abstract class NonCleartextPassword extends DataFlow::Node { }
157+
158+
/**
159+
* An object with a property that may contain password information
160+
*
161+
* This is a source since `toString()` on this object will show the property value.
162+
*/
163+
private class ObjectPasswordPropertySource extends DataFlow::ValueNode, Source {
164+
string name;
165+
166+
ObjectPasswordPropertySource() {
167+
exists (DataFlow::PropWrite write |
168+
write.getPropertyName() = name and
169+
name.regexpMatch(suspiciousPassword()) and
170+
not name.regexpMatch(nonSuspicious()) and
171+
this.(DataFlow::SourceNode).flowsTo(write.getBase()) and
172+
// avoid safe values assigned to presumably unsafe names
173+
not write.getRhs() instanceof NonCleartextPassword
174+
)
175+
}
176+
177+
override string describe() {
178+
result = "an access to " + name
179+
}
180+
}
181+
182+
/** An access to a variable or property that might contain a password. */
183+
private class ReadPasswordSource extends DataFlow::ValueNode, Source {
184+
string name;
185+
186+
ReadPasswordSource() {
187+
// avoid safe values assigned to presumably unsafe names
188+
not this instanceof NonCleartextPassword and
189+
name.regexpMatch(suspiciousPassword()) and
190+
(
191+
this.asExpr().(VarAccess).getName() = name
192+
or
193+
exists (DataFlow::PropRead read, DataFlow::Node base |
194+
this = read and
195+
base = read.getBase() and
196+
read.getPropertyName() = name and
197+
// avoid safe values assigned to presumably unsafe names
198+
exists (DataFlow::SourceNode baseObj | baseObj.flowsTo(base) |
199+
not baseObj.getAPropertyWrite(name).getRhs() instanceof NonCleartextPassword
200+
)
201+
)
202+
)
203+
}
204+
205+
override string describe() {
206+
result = "an access to " + name
207+
}
208+
}
209+
210+
/** A call that might return a password. */
211+
private class CallPasswordSource extends DataFlow::ValueNode, DataFlow::InvokeNode, Source {
212+
string name;
213+
214+
CallPasswordSource() {
215+
name = getCalleeName() and
216+
name.regexpMatch("(?is)getPassword")
217+
}
218+
219+
override string describe() {
220+
result = "a call to " + name
221+
}
222+
}
223+
224+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
| passwords.js:2:17:2:24 | password | Sensitive data returned by $@ is logged here. | passwords.js:2:17:2:24 | password | an access to password |
2+
| passwords.js:3:17:3:26 | o.password | Sensitive data returned by $@ is logged here. | passwords.js:3:17:3:26 | o.password | an access to password |
3+
| passwords.js:4:17:4:29 | getPassword() | Sensitive data returned by $@ is logged here. | passwords.js:4:17:4:29 | getPassword() | a call to getPassword |
4+
| passwords.js:5:17:5:31 | o.getPassword() | Sensitive data returned by $@ is logged here. | passwords.js:5:17:5:31 | o.getPassword() | a call to getPassword |
5+
| passwords.js:8:21:8:21 | x | Sensitive data returned by $@ is logged here. | passwords.js:10:11:10:18 | password | an access to password |
6+
| passwords.js:12:18:12:25 | password | Sensitive data returned by $@ is logged here. | passwords.js:12:18:12:25 | password | an access to password |
7+
| passwords.js:14:17:14:38 | name + ... assword | Sensitive data returned by $@ is logged here. | passwords.js:14:31:14:38 | password | an access to password |
8+
| passwords.js:16:17:16:38 | `${name ... sword}` | Sensitive data returned by $@ is logged here. | passwords.js:16:29:16:36 | password | an access to password |
9+
| passwords.js:21:17:21:20 | obj1 | Sensitive data returned by $@ is logged here. | passwords.js:18:16:20:5 | {\\n ... x\\n } | an access to password |
10+
| passwords.js:26:17:26:20 | obj2 | Sensitive data returned by $@ is logged here. | passwords.js:24:12:24:19 | password | an access to password |
11+
| passwords.js:29:17:29:20 | obj3 | Sensitive data returned by $@ is logged here. | passwords.js:30:14:30:21 | password | an access to password |
12+
| passwords.js:78:17:78:38 | temp.en ... assword | Sensitive data returned by $@ is logged here. | passwords.js:77:37:77:53 | req.body.password | an access to password |
13+
| passwords.js:81:17:81:31 | `pw: ${secret}` | Sensitive data returned by $@ is logged here. | passwords.js:80:18:80:25 | password | an access to password |
14+
| passwords.js:114:25:114:50 | "Passwo ... assword | Sensitive data returned by $@ is logged here. | passwords.js:114:43:114:50 | password | an access to password |
15+
| passwords_in_server_1.js:6:13:6:20 | password | Sensitive data returned by $@ is logged here. | passwords_in_server_1.js:6:13:6:20 | password | an access to password |
16+
| passwords_in_server_2.js:3:13:3:20 | password | Sensitive data returned by $@ is logged here. | passwords_in_server_2.js:3:13:3:20 | password | an access to password |
17+
| passwords_in_server_3.js:2:13:2:20 | password | Sensitive data returned by $@ is logged here. | passwords_in_server_3.js:2:13:2:20 | password | an access to password |
18+
| passwords_in_server_4.js:2:13:2:20 | password | Sensitive data returned by $@ is logged here. | passwords_in_server_4.js:2:13:2:20 | password | an access to password |
19+
| passwords_in_server_5.js:8:17:8:17 | x | Sensitive data returned by $@ is logged here. | passwords_in_server_5.js:4:7:4:24 | req.query.password | an access to password |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-312/CleartextLogging.ql
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import foo from "foo";
2+
window.location;

0 commit comments

Comments
 (0)