Skip to content

Commit 28d8011

Browse files
author
Max Schaefer
committed
JavaScript: Add models for popular base64 transcoders.
1 parent 6baf526 commit 28d8011

File tree

15 files changed

+302
-0
lines changed

15 files changed

+302
-0
lines changed

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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Provides classes and predicates for working with popular 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+
/** A call to `Buffer.from` with encoding `base64`. */
101+
private class Buffer_from extends Base64::Encode::Range, DataFlow::CallNode {
102+
Buffer_from() {
103+
this = DataFlow::globalVarRef("Buffer").getAMemberCall("from") and
104+
getArgument(1).mayHaveStringValue("base64")
105+
}
106+
107+
override DataFlow::Node getInput() { result = getArgument(0) }
108+
109+
override DataFlow::Node getOutput() { result = this }
110+
}
111+
112+
/**
113+
* A call to `Buffer.prototype.toString` with encoding `base64`, approximated by
114+
* looking for calls to `toString` where the first argument is the string `"base64"`.
115+
*/
116+
private class Buffer_toString extends Base64::Decode::Range, DataFlow::MethodCallNode {
117+
Buffer_toString() {
118+
getMethodName() = "toString" and
119+
getArgument(0).mayHaveStringValue("base64")
120+
}
121+
122+
override DataFlow::Node getInput() { result = getReceiver() }
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(string mod, string meth |
134+
mod = "base-64" and meth = "encode"
135+
or
136+
mod = "Base64" and meth = "btoa"
137+
or
138+
mod = "base64-js" and meth = "toByteArray"
139+
|
140+
this = DataFlow::moduleMember(mod, meth).getACall()
141+
)
142+
or
143+
exists(string meth | meth = "encode" or meth = "encodeURI" |
144+
this = DataFlow::moduleMember("js-base64", "Base64").getAMemberCall(meth)
145+
)
146+
}
147+
148+
override DataFlow::Node getInput() { result = getArgument(0) }
149+
150+
override DataFlow::Node getOutput() { result = this }
151+
}
152+
153+
/**
154+
* A call to a base64 decoding function from one of the npm packages
155+
* `base-64`, `js-base64`, `Base64`, or `base64-js`.
156+
*/
157+
private class NpmBase64Decode extends Base64::Decode::Range, DataFlow::CallNode {
158+
NpmBase64Decode() {
159+
exists(string mod, string meth |
160+
mod = "base-64" and meth = "decode"
161+
or
162+
mod = "Base64" and meth = "atob"
163+
or
164+
mod = "base64-js" and meth = "fromByteArray"
165+
|
166+
this = DataFlow::moduleMember(mod, meth).getACall()
167+
)
168+
or
169+
this = DataFlow::moduleMember("js-base64", "Base64").getAMemberCall("decode")
170+
}
171+
172+
override DataFlow::Node getInput() { result = getArgument(0) }
173+
174+
override DataFlow::Node getOutput() { result = this }
175+
}
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: 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+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var base64 = require('base64-js');
2+
3+
function roundtrip(data) {
4+
var encoded = base64.toByteArray(data);
5+
return base64.fromByteArray(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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
function roundtrip(data) {
2+
var encoded = btoa(data);
3+
return atob(encoded);
4+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var base64 = require('js-base64').Base64;
2+
3+
function roundtrip1(data) {
4+
var encoded = base64.encode(data);
5+
return base64.decode(encoded);
6+
}
7+
8+
function roundtrip2(data) {
9+
var encoded = base64.encodeURI(data);
10+
return base64.decode(encoded);
11+
}

0 commit comments

Comments
 (0)