Skip to content

Commit 807fc94

Browse files
authored
Merge pull request #4921 from erik-krogh/moreShellSan
Approved by esbena
2 parents 3b16d26 + 6423c32 commit 807fc94

File tree

3 files changed

+103
-5
lines changed

3 files changed

+103
-5
lines changed

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

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,16 +161,19 @@ module UnsafeShellCommandConstruction {
161161
}
162162
}
163163

164+
/**
165+
* Gets an unsafe shell character.
166+
*/
167+
private string getAShellChar() {
168+
result = ["&", "`", "$", "|", ">", "<", "#", ";", "(", ")", "[", "]", "\n"]
169+
}
170+
164171
/**
165172
* A chain of replace calls that replaces all unsafe chars for shell-commands.
166173
*/
167174
class ChainSanitizer extends Sanitizer, IncompleteBlacklistSanitizer::StringReplaceCallSequence {
168175
ChainSanitizer() {
169-
forall(string char |
170-
char = ["&", "`", "$", "|", ">", "<", "#", ";", "(", ")", "[", "]", "\n"]
171-
|
172-
this.getAMember().getAReplacedString() = char
173-
)
176+
forall(string char | char = getAShellChar() | this.getAMember().getAReplacedString() = char)
174177
}
175178
}
176179

@@ -208,4 +211,50 @@ module UnsafeShellCommandConstruction {
208211
e = x
209212
}
210213
}
214+
215+
private import semmle.javascript.dataflow.internal.AccessPaths
216+
private import semmle.javascript.dataflow.InferredTypes
217+
218+
/**
219+
* Holds if `instance` is an instance of the access-path `ap`, and there exists a guard
220+
* that ensures that `instance` is not equal to `char`.
221+
*
222+
* For example if `ap` is `str[i]` and `char` is `<`:
223+
* ```JavaScript
224+
* if (str[i] !== "<" && ...) {
225+
* var foo = str[i]; // <- `instance`
226+
* }
227+
* ```
228+
* or
229+
* ```JavaScript
230+
* if (!(str[i] == "<" || ...)) {
231+
* var foo = str[i]; // <- `instance`
232+
* }
233+
* ```
234+
*/
235+
private predicate blocksCharInAccess(AccessPath ap, string char, Expr instance) {
236+
exists(BasicBlock bb, ConditionGuardNode guard, EqualityTest test |
237+
test.getAnOperand().mayHaveStringValue(char) and
238+
char = getAShellChar() and
239+
guard.getTest() = test and
240+
guard.dominates(bb) and
241+
test.getAnOperand() = ap.getAnInstance() and
242+
instance = ap.getAnInstanceIn(bb) and
243+
guard.getOutcome() != test.getPolarity()
244+
)
245+
}
246+
247+
/**
248+
* A sanitizer for a single character, where the character cannot be an unsafe shell character.
249+
*/
250+
class SanitizedChar extends Sanitizer, DataFlow::ValueNode {
251+
override PropAccess astNode;
252+
253+
SanitizedChar() {
254+
exists(AccessPath ap | this.asExpr() = ap.getAnInstance() |
255+
forall(string char | char = getAShellChar() | blocksCharInAccess(ap, char, astNode))
256+
) and
257+
astNode.getPropertyNameExpr().analyze().getTheType() = TTNumber()
258+
}
259+
}
211260
}

javascript/ql/test/query-tests/Security/CWE-078/UnsafeShellCommandConstruction.expected

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ nodes
201201
| lib/lib.js:361:20:361:34 | opts.learn_args |
202202
| lib/lib.js:366:28:366:42 | this.learn_args |
203203
| lib/lib.js:366:28:366:42 | this.learn_args |
204+
| lib/lib.js:405:39:405:42 | name |
205+
| lib/lib.js:405:39:405:42 | name |
206+
| lib/lib.js:406:22:406:25 | name |
207+
| lib/lib.js:406:22:406:25 | name |
204208
edges
205209
| lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name |
206210
| lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name |
@@ -436,6 +440,10 @@ edges
436440
| lib/lib.js:361:20:361:23 | opts | lib/lib.js:361:20:361:34 | opts.learn_args |
437441
| lib/lib.js:361:20:361:34 | opts.learn_args | lib/lib.js:366:28:366:42 | this.learn_args |
438442
| lib/lib.js:361:20:361:34 | opts.learn_args | lib/lib.js:366:28:366:42 | this.learn_args |
443+
| lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name |
444+
| lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name |
445+
| lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name |
446+
| lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name |
439447
#select
440448
| lib/lib2.js:4:10:4:25 | "rm -rf " + name | lib/lib2.js:3:28:3:31 | name | lib/lib2.js:4:22:4:25 | name | $@ based on library input is later used in $@. | lib/lib2.js:4:10:4:25 | "rm -rf " + name | String concatenation | lib/lib2.js:4:2:4:26 | cp.exec ... + name) | shell command |
441449
| lib/lib2.js:8:10:8:25 | "rm -rf " + name | lib/lib2.js:7:32:7:35 | name | lib/lib2.js:8:22:8:25 | name | $@ based on library input is later used in $@. | lib/lib2.js:8:10:8:25 | "rm -rf " + name | String concatenation | lib/lib2.js:8:2:8:26 | cp.exec ... + name) | shell command |
@@ -493,3 +501,4 @@ edges
493501
| lib/lib.js:340:10:340:26 | "rm -rf " + id(n) | lib/lib.js:339:39:339:39 | n | lib/lib.js:340:22:340:26 | id(n) | $@ based on library input is later used in $@. | lib/lib.js:340:10:340:26 | "rm -rf " + id(n) | String concatenation | lib/lib.js:340:2:340:27 | cp.exec ... id(n)) | shell command |
494502
| lib/lib.js:351:10:351:27 | "rm -rf " + unsafe | lib/lib.js:349:29:349:34 | unsafe | lib/lib.js:351:22:351:27 | unsafe | $@ based on library input is later used in $@. | lib/lib.js:351:10:351:27 | "rm -rf " + unsafe | String concatenation | lib/lib.js:351:2:351:28 | cp.exec ... unsafe) | shell command |
495503
| lib/lib.js:366:17:366:56 | "learn ... + model | lib/lib.js:360:20:360:23 | opts | lib/lib.js:366:28:366:42 | this.learn_args | $@ based on library input is later used in $@. | lib/lib.js:366:17:366:56 | "learn ... + model | String concatenation | lib/lib.js:367:3:367:18 | cp.exec(command) | shell command |
504+
| lib/lib.js:406:10:406:25 | "rm -rf " + name | lib/lib.js:405:39:405:42 | name | lib/lib.js:406:22:406:25 | name | $@ based on library input is later used in $@. | lib/lib.js:406:10:406:25 | "rm -rf " + name | String concatenation | lib/lib.js:406:2:406:26 | cp.exec ... + name) | shell command |

javascript/ql/test/query-tests/Security/CWE-078/lib/lib.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,43 @@ MyTrainer.prototype = {
368368
}
369369
};
370370
module.exports.MyTrainer = MyTrainer;
371+
372+
373+
function yetAnohterSanitizer(str) {
374+
const s = str || '';
375+
let result = '';
376+
for (let i = 0; i <= 2000; i++) {
377+
if (!(s[i] === undefined ||
378+
s[i] === '>' ||
379+
s[i] === '<' ||
380+
s[i] === '*' ||
381+
s[i] === '?' ||
382+
s[i] === '[' ||
383+
s[i] === ']' ||
384+
s[i] === '|' ||
385+
s[i] === '˚' ||
386+
s[i] === '$' ||
387+
s[i] === ';' ||
388+
s[i] === '&' ||
389+
s[i] === '(' ||
390+
s[i] === ')' ||
391+
s[i] === ']' ||
392+
s[i] === '#' ||
393+
s[i] === '\\' ||
394+
s[i] === '\t' ||
395+
s[i] === '\n' ||
396+
s[i] === '\'' ||
397+
s[i] === '`' ||
398+
s[i] === '"')) {
399+
result = result + s[i];
400+
}
401+
}
402+
return result;
403+
}
404+
405+
module.exports.sanitizer3 = function (name) {
406+
cp.exec("rm -rf " + name); // NOT OK
407+
408+
var sanitized = yetAnohterSanitizer(name);
409+
cp.exec("rm -rf " + sanitized); // OK
410+
}

0 commit comments

Comments
 (0)