Skip to content

Commit 7513bcf

Browse files
authored
Merge pull request #1095 from xiemaisi/js/base64
Approved by esben-semmle
2 parents 6a555d0 + 5d35626 commit 7513bcf

File tree

18 files changed

+332
-2
lines changed

18 files changed

+332
-2
lines changed

change-notes/1.21/analysis-javascript.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Support for the following frameworks and libraries has been improved:
66
- [socket.io](http://socket.io)
77

8+
* The security queries now track data flow through Base64 decoders such as the Node.js `Buffer` class, the DOM function `atob`, and a number of npm packages intcluding [`abab`](https://www.npmjs.com/package/abab), [`atob`](https://www.npmjs.com/package/atob), [`btoa`](https://www.npmjs.com/package/btoa), [`base-64`](https://www.npmjs.com/package/base-64), [`js-base64`](https://www.npmjs.com/package/js-base64), [`Base64.js`](https://www.npmjs.com/package/Base64) and [`base64-js`](https://www.npmjs.com/package/base64-js).
9+
10+
811
## New queries
912

1013
| **Query** | **Tags** | **Purpose** |

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import semmle.javascript.Aliases
66
import semmle.javascript.AMD
77
import semmle.javascript.AST
88
import semmle.javascript.BasicBlocks
9+
import semmle.javascript.Base64
910
import semmle.javascript.CFG
1011
import semmle.javascript.Classes
1112
import semmle.javascript.Closure
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Provides classes and predicates for working with base64 encoders and decoders.
3+
*/
4+
5+
import javascript
6+
7+
module Base64 {
8+
/** A call to a base64 encoder. */
9+
class Encode extends DataFlow::Node {
10+
Encode::Range encode;
11+
12+
Encode() { this = encode }
13+
14+
/** Gets the input passed to the encoder. */
15+
DataFlow::Node getInput() { result = encode.getInput() }
16+
17+
/** Gets the base64-encoded output of the encoder. */
18+
DataFlow::Node getOutput() { result = encode.getOutput() }
19+
}
20+
21+
module Encode {
22+
/**
23+
* A data flow node that should be considered a call to a base64 encoder.
24+
*
25+
* New base64 encoding functions can be supported by extending this class.
26+
*/
27+
abstract class Range extends DataFlow::Node {
28+
/** Gets the input passed to the encoder. */
29+
abstract DataFlow::Node getInput();
30+
31+
/** Gets the base64-encoded output of the encoder. */
32+
abstract DataFlow::Node getOutput();
33+
}
34+
}
35+
36+
/** A call to a base64 decoder. */
37+
class Decode extends DataFlow::Node {
38+
Decode::Range encode;
39+
40+
Decode() { this = encode }
41+
42+
/** Gets the base64-encoded input passed to the decoder. */
43+
DataFlow::Node getInput() { result = encode.getInput() }
44+
45+
/** Gets the output of the decoder. */
46+
DataFlow::Node getOutput() { result = encode.getOutput() }
47+
}
48+
49+
module Decode {
50+
/**
51+
* A data flow node that should be considered a call to a base64 decoder.
52+
*
53+
* New base64 decoding functions can be supported by extending this class.
54+
*/
55+
abstract class Range extends DataFlow::Node {
56+
/** Gets the base64-encoded input passed to the decoder. */
57+
abstract DataFlow::Node getInput();
58+
59+
/** Gets the output of the decoder. */
60+
abstract DataFlow::Node getOutput();
61+
}
62+
}
63+
64+
/**
65+
* A base64 decoding step, viewed as a taint-propagating data flow edge.
66+
*
67+
* Note that we currently do not model base64 encoding as a taint-propagating data flow edge
68+
* to avoid false positives.
69+
*/
70+
private class Base64DecodingStep extends TaintTracking::AdditionalTaintStep {
71+
Decode dec;
72+
73+
Base64DecodingStep() { this = dec }
74+
75+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
76+
pred = dec.getInput() and
77+
succ = dec.getOutput()
78+
}
79+
}
80+
}
81+
82+
/** A call to `btoa`. */
83+
private class Btoa extends Base64::Encode::Range, DataFlow::CallNode {
84+
Btoa() { this = DataFlow::globalVarRef("btoa").getACall() }
85+
86+
override DataFlow::Node getInput() { result = getArgument(0) }
87+
88+
override DataFlow::Node getOutput() { result = this }
89+
}
90+
91+
/** A call to `atob`. */
92+
private class Atob extends Base64::Decode::Range, DataFlow::CallNode {
93+
Atob() { this = DataFlow::globalVarRef("atob").getACall() }
94+
95+
override DataFlow::Node getInput() { result = getArgument(0) }
96+
97+
override DataFlow::Node getOutput() { result = this }
98+
}
99+
100+
/**
101+
* A call to `Buffer.prototype.toString` with encoding `base64`, approximated by
102+
* looking for calls to `toString` where the first argument is the string `"base64"`.
103+
*/
104+
private class Buffer_toString extends Base64::Encode::Range, DataFlow::MethodCallNode {
105+
Buffer_toString() {
106+
getMethodName() = "toString" and
107+
getArgument(0).mayHaveStringValue("base64")
108+
}
109+
110+
override DataFlow::Node getInput() { result = getReceiver() }
111+
112+
override DataFlow::Node getOutput() { result = this }
113+
}
114+
115+
/** A call to `Buffer.from` with encoding `base64`. */
116+
private class Buffer_from extends Base64::Decode::Range, DataFlow::CallNode {
117+
Buffer_from() {
118+
this = DataFlow::globalVarRef("Buffer").getAMemberCall("from") and
119+
getArgument(1).mayHaveStringValue("base64")
120+
}
121+
122+
override DataFlow::Node getInput() { result = getArgument(0) }
123+
124+
override DataFlow::Node getOutput() { result = this }
125+
}
126+
127+
/**
128+
* A call to a base64 encoding function from one of the npm packages
129+
* `base-64`, `js-base64`, `Base64`, or `base64-js`.
130+
*/
131+
private class NpmBase64Encode extends Base64::Encode::Range, DataFlow::CallNode {
132+
NpmBase64Encode() {
133+
exists(DataFlow::SourceNode enc |
134+
enc = DataFlow::moduleImport("b64u") or
135+
enc = DataFlow::moduleImport("b64url") or
136+
enc = DataFlow::moduleImport("btoa") or
137+
enc = DataFlow::moduleMember("Base64", "btoa") or
138+
enc = DataFlow::moduleMember("abab", "btoa") or
139+
enc = DataFlow::moduleMember("b2a", "btoa") or
140+
enc = DataFlow::moduleMember("b64-lite", "btoa") or
141+
enc = DataFlow::moduleMember("b64-lite", "toBase64") or
142+
enc = DataFlow::moduleMember("b64u", "encode") or
143+
enc = DataFlow::moduleMember("b64u", "toBase64") or
144+
enc = DataFlow::moduleMember("b64u-lite", "toBase64Url") or
145+
enc = DataFlow::moduleMember("b64u-lite", "toBinaryString") or
146+
enc = DataFlow::moduleMember("b64url", "encode") or
147+
enc = DataFlow::moduleMember("b64url", "toBase64") or
148+
enc = DataFlow::moduleMember("base-64", "encode") or
149+
enc = DataFlow::moduleMember("base64-js", "toByteArray") or
150+
enc = DataFlow::moduleMember("base64-url", "encode") or
151+
enc = DataFlow::moduleMember("base64url", "encode") or
152+
enc = DataFlow::moduleMember("base64url", "toBase64") or
153+
enc = DataFlow::moduleMember("js-base64", "Base64").getAPropertyRead("encode") or
154+
enc = DataFlow::moduleMember("js-base64", "Base64").getAPropertyRead("encodeURI") or
155+
enc = DataFlow::moduleMember("urlsafe-base64", "encode")
156+
|
157+
this = enc.getACall()
158+
)
159+
}
160+
161+
override DataFlow::Node getInput() { result = getArgument(0) }
162+
163+
override DataFlow::Node getOutput() { result = this }
164+
}
165+
166+
/**
167+
* A call to a base64 decoding function from one of the npm packages
168+
* `base-64`, `js-base64`, `Base64`, or `base64-js`.
169+
*/
170+
private class NpmBase64Decode extends Base64::Decode::Range, DataFlow::CallNode {
171+
NpmBase64Decode() {
172+
exists(DataFlow::SourceNode dec |
173+
dec = DataFlow::moduleImport("atob") or
174+
dec = DataFlow::moduleMember("Base64", "atob") or
175+
dec = DataFlow::moduleMember("abab", "atob") or
176+
dec = DataFlow::moduleMember("b2a", "atob") or
177+
dec = DataFlow::moduleMember("b64-lite", "atob") or
178+
dec = DataFlow::moduleMember("b64-lite", "fromBase64") or
179+
dec = DataFlow::moduleMember("b64u", "decode") or
180+
dec = DataFlow::moduleMember("b64u", "fromBase64") or
181+
dec = DataFlow::moduleMember("b64u-lite", "fromBase64Url") or
182+
dec = DataFlow::moduleMember("b64u-lite", "fromBinaryString") or
183+
dec = DataFlow::moduleMember("b64url", "decode") or
184+
dec = DataFlow::moduleMember("b64url", "fromBase64") or
185+
dec = DataFlow::moduleMember("base-64", "decode") or
186+
dec = DataFlow::moduleMember("base64-js", "fromByteArray") or
187+
dec = DataFlow::moduleMember("base64-url", "decode") or
188+
dec = DataFlow::moduleMember("base64url", "decode") or
189+
dec = DataFlow::moduleMember("base64url", "fromBase64") or
190+
dec = DataFlow::moduleMember("js-base64", "Base64").getAPropertyRead("decode") or
191+
dec = DataFlow::moduleMember("urlsafe-base64", "decode")
192+
|
193+
this = dec.getACall()
194+
)
195+
}
196+
197+
override DataFlow::Node getInput() { result = getArgument(0) }
198+
199+
override DataFlow::Node getOutput() { result = this }
200+
}

javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ module TaintTracking {
443443
(mce.getMethodName() = "fromCharCode" or mce.getMethodName() = "fromCodePoint")
444444
)
445445
or
446-
// `(encode|decode)URI(Component)?` and `escape` propagate taint
446+
// `(encode|decode)URI(Component)?` propagate taint
447447
exists(DataFlow::CallNode c, string name |
448448
this = c and
449449
c = DataFlow::globalVarRef(name).getACall() and

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Provides predicates for reasoning about DOM types.
2+
* Provides predicates for reasoning about DOM types and methods.
33
*/
44

55
import javascript
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var Base64 = require("Base64");
2+
3+
function roundtrip(data) {
4+
var encoded = Base64.btoa(data);
5+
return Base64.atob(encoded);
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function roundtrip(data) {
2+
var encoded = Buffer.from(data, 'base64');
3+
return encoded.toString('base64');
4+
}
5+
6+
function roundtrip2(data) {
7+
var encoded = Buffer.from(data, 'hex');
8+
return encoded.toString('hex');
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import javascript
2+
3+
query Base64::Decode test_Decode() {
4+
any()
5+
}
6+
7+
query predicate test_Decode_input_output(Base64::Decode decode, DataFlow::Node input, DataFlow::Node output) {
8+
input = decode.getInput() and
9+
output = decode.getOutput()
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import javascript
2+
3+
query Base64::Encode test_Encode() {
4+
any()
5+
}
6+
7+
query predicate test_Encode_input_output(Base64::Encode encode, DataFlow::Node input, DataFlow::Node output) {
8+
input = encode.getInput() and
9+
output = encode.getOutput()
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var base64 = require('base-64');
2+
3+
function roundtrip(data) {
4+
var encoded = base64.encode(data);
5+
return base64.decode(encoded);
6+
}

0 commit comments

Comments
 (0)