Skip to content

Commit c69a61b

Browse files
committed
Python: Model exec and eval calls as CodeExecution
1 parent 73971cf commit c69a61b

File tree

7 files changed

+102
-9
lines changed

7 files changed

+102
-9
lines changed

python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,98 @@ private module Stdlib {
327327
)
328328
}
329329
}
330+
331+
// ---------------------------------------------------------------------------
332+
// builtins
333+
// ---------------------------------------------------------------------------
334+
/** Gets a reference to the `builtins` module (called `__builtin__` in Python 2). */
335+
private DataFlow::Node builtins(DataFlow::TypeTracker t) {
336+
t.start() and
337+
result = DataFlow::importModule(["builtins", "__builtin__"])
338+
or
339+
exists(DataFlow::TypeTracker t2 | result = builtins(t2).track(t2, t))
340+
}
341+
342+
/** Gets a reference to the `builtins` module. */
343+
DataFlow::Node builtins() { result = builtins(DataFlow::TypeTracker::end()) }
344+
345+
/**
346+
* Gets a reference to the attribute `attr_name` of the `builtins` module.
347+
* WARNING: Only holds for a few predefined attributes.
348+
*/
349+
private DataFlow::Node builtins_attr(DataFlow::TypeTracker t, string attr_name) {
350+
attr_name in ["exec", "eval"] and
351+
(
352+
t.start() and
353+
result = DataFlow::importMember(["builtins", "__builtin__"], attr_name)
354+
or
355+
t.startInAttr(attr_name) and
356+
result = DataFlow::importModule(["builtins", "__builtin__"])
357+
or
358+
// special handling of builtins, that are in scope without any imports
359+
// TODO: Take care of overrides, either `def eval: ...`, `eval = ...`, or `builtins.eval = ...`
360+
t.start() and
361+
exists(NameNode ref | result.asCfgNode() = ref |
362+
ref.isGlobal() and
363+
ref.getId() = attr_name and
364+
ref.isLoad()
365+
)
366+
)
367+
or
368+
// Due to bad performance when using normal setup with `builtins_attr(t2, attr_name).track(t2, t)`
369+
// we have inlined that code and forced a join
370+
exists(DataFlow::TypeTracker t2 |
371+
exists(DataFlow::StepSummary summary |
372+
builtins_attr_first_join(t2, attr_name, result, summary) and
373+
t = t2.append(summary)
374+
)
375+
)
376+
}
377+
378+
pragma[nomagic]
379+
private predicate builtins_attr_first_join(
380+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, DataFlow::StepSummary summary
381+
) {
382+
DataFlow::StepSummary::step(builtins_attr(t2, attr_name), res, summary)
383+
}
384+
385+
/**
386+
* Gets a reference to the attribute `attr_name` of the `builtins` module.
387+
* WARNING: Only holds for a few predefined attributes.
388+
*/
389+
private DataFlow::Node builtins_attr(string attr_name) {
390+
result = builtins_attr(DataFlow::TypeTracker::end(), attr_name)
391+
}
392+
393+
/**
394+
* A call to the builtin `exec` function.
395+
* See https://docs.python.org/3/library/functions.html#exec
396+
*/
397+
private class BuiltinsExecCall extends CodeExecution::Range {
398+
CallNode call;
399+
400+
BuiltinsExecCall() {
401+
this.asCfgNode() = call and
402+
call.getFunction() = builtins_attr("exec").asCfgNode()
403+
}
404+
405+
override DataFlow::Node getCode() { result.asCfgNode() = call.getArg(0) }
406+
}
407+
408+
/**
409+
* A call to the builtin `eval` function.
410+
* See https://docs.python.org/3/library/functions.html#eval
411+
*/
412+
private class BuiltinsEvalCall extends CodeExecution::Range {
413+
CallNode call;
414+
415+
BuiltinsEvalCall() {
416+
this.asCfgNode() = call and
417+
call.getFunction() = builtins_attr("eval").asCfgNode()
418+
}
419+
420+
override DataFlow::Node getCode() { result.asCfgNode() = call.getArg(0) }
421+
}
330422
}
331423

332424
/**
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
| CodeExecution.py:4:29:4:50 | Comment # $getCode="print(42)" | Missing result:getCode="print(42)" |

python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ def eval(*args, **kwargs):
88

99

1010
# This function call might be marked as a code execution, but it actually isn't.
11-
eval("print(42)")
11+
eval("print(42)") # $f+:getCode="print(42)"

python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ def foo(*args, **kwargs):
1010
eval = foo
1111

1212
# This function call might be marked as a code execution, but it actually isn't.
13-
eval("print(42)")
13+
eval("print(42)") # $f+:getCode="print(42)"

python/ql/test/experimental/library-tests/frameworks/stdlib/CodeExecutionPossibleFP3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ def foo(*args, **kwargs):
1616
builtins.eval = foo
1717

1818
# This function call might be marked as a code execution, but it actually isn't.
19-
eval("print(42)")
19+
eval("print(42)") # $f+:getCode="print(42)"
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
| CodeExecution.py:12:20:12:41 | Comment # $getCode="print(42)" | Missing result:getCode="print(42)" |
2-
| CodeExecution.py:13:20:13:41 | Comment # $getCode="print(42)" | Missing result:getCode="print(42)" |
3-
| CodeExecution.py:15:29:15:50 | Comment # $getCode="print(42)" | Missing result:getCode="print(42)" |
4-
| CodeExecution.py:18:12:18:25 | Comment # $getCode=cmd | Missing result:getCode=cmd |
5-
| CodeExecution.py:21:12:21:25 | Comment # $getCode=cmd | Missing result:getCode=cmd |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
edges
2+
| code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | code_injection.py:7:10:7:13 | ControlFlowNode for code |
3+
| code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | code_injection.py:8:10:8:13 | ControlFlowNode for code |
24
nodes
5+
| code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
6+
| code_injection.py:7:10:7:13 | ControlFlowNode for code | semmle.label | ControlFlowNode for code |
7+
| code_injection.py:8:10:8:13 | ControlFlowNode for code | semmle.label | ControlFlowNode for code |
38
#select
9+
| code_injection.py:7:10:7:13 | ControlFlowNode for code | code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | code_injection.py:7:10:7:13 | ControlFlowNode for code | $@ flows to here and is interpreted as code. | code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | A user-provided value |
10+
| code_injection.py:8:10:8:13 | ControlFlowNode for code | code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | code_injection.py:8:10:8:13 | ControlFlowNode for code | $@ flows to here and is interpreted as code. | code_injection.py:6:12:6:23 | ControlFlowNode for Attribute | A user-provided value |

0 commit comments

Comments
 (0)