Skip to content

Commit aeba497

Browse files
authored
Merge pull request #7735 from yoff/python/promote-log-injection
Python: promote log injection
2 parents 3ce7d47 + b59ab7f commit aeba497

File tree

19 files changed

+208
-205
lines changed

19 files changed

+208
-205
lines changed

python/ql/lib/semmle/python/frameworks/Django.qll

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,4 +2295,22 @@ module PrivateDjango {
22952295

22962296
override string getMimetypeDefault() { none() }
22972297
}
2298+
2299+
// ---------------------------------------------------------------------------
2300+
// Logging
2301+
// ---------------------------------------------------------------------------
2302+
/**
2303+
* A standard Python logger instance from Django.
2304+
* see https://github.com/django/django/blob/stable/4.0.x/django/utils/log.py#L11
2305+
*/
2306+
private class DjangoLogger extends Stdlib::Logger::InstanceSource {
2307+
DjangoLogger() {
2308+
this =
2309+
API::moduleImport("django")
2310+
.getMember("utils")
2311+
.getMember("log")
2312+
.getMember("request_logger")
2313+
.getAnImmediateUse()
2314+
}
2315+
}
22982316
}

python/ql/lib/semmle/python/frameworks/Flask.qll

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.RemoteFlowSources
99
private import semmle.python.dataflow.new.TaintTracking
1010
private import semmle.python.Concepts
1111
private import semmle.python.frameworks.Werkzeug
12+
private import semmle.python.frameworks.Stdlib
1213
private import semmle.python.ApiGraphs
1314
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
1415
private import semmle.python.security.dataflow.PathInjectionCustomizations
@@ -569,4 +570,18 @@ module Flask {
569570
result in [this.getArg(0), this.getArgByName("filename_or_fp")]
570571
}
571572
}
573+
574+
// ---------------------------------------------------------------------------
575+
// Logging
576+
// ---------------------------------------------------------------------------
577+
/**
578+
* A Flask application provides a standard Python logger via the `logger` attribute.
579+
*
580+
* See
581+
* - https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.logger
582+
* - https://flask.palletsprojects.com/en/2.0.x/logging/
583+
*/
584+
private class FlaskLogger extends Stdlib::Logger::InstanceSource {
585+
FlaskLogger() { this = FlaskApp::instance().getMember("logger").getAnImmediateUse() }
586+
}
572587
}

python/ql/lib/semmle/python/frameworks/Stdlib.qll

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,54 @@ module Stdlib {
237237
}
238238
}
239239
}
240+
241+
// ---------------------------------------------------------------------------
242+
// logging
243+
// ---------------------------------------------------------------------------
244+
/**
245+
* Provides models for the `logging.Logger` class and subclasses.
246+
*
247+
* See https://docs.python.org/3.9/library/logging.html#logging.Logger.
248+
*/
249+
module Logger {
250+
/** Gets a reference to the `logging.Logger` class or any subclass. */
251+
private API::Node subclassRef() {
252+
result = API::moduleImport("logging").getMember("Logger").getASubclass*()
253+
}
254+
255+
/**
256+
* A source of instances of `logging.Logger`, extend this class to model new instances.
257+
*
258+
* This can include instantiations of the class, return values from function
259+
* calls, or a special parameter that will be set when functions are called by an external
260+
* library.
261+
*
262+
* Use the predicate `Logger::instance()` to get references to instances of `logging.Logger`.
263+
*/
264+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
265+
266+
/** A direct instantiation of `logging.Logger`. */
267+
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
268+
ClassInstantiation() {
269+
this = subclassRef().getACall()
270+
or
271+
this = API::moduleImport("logging").getMember("root").getAnImmediateUse()
272+
or
273+
this = API::moduleImport("logging").getMember("getLogger").getACall()
274+
}
275+
}
276+
277+
/** Gets a reference to an instance of `logging.Logger`. */
278+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
279+
t.start() and
280+
result instanceof InstanceSource
281+
or
282+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
283+
}
284+
285+
/** Gets a reference to an instance of `logging.Logger`. */
286+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
287+
}
240288
}
241289

242290
/**
@@ -2642,27 +2690,6 @@ private module StdlibPrivate {
26422690
// ---------------------------------------------------------------------------
26432691
// logging
26442692
// ---------------------------------------------------------------------------
2645-
/**
2646-
* Provides models for the `logging.Logger` class and subclasses.
2647-
*
2648-
* See https://docs.python.org/3.9/library/logging.html#logging.Logger.
2649-
*/
2650-
module Logger {
2651-
/** Gets a reference to the `logging.Logger` class or any subclass. */
2652-
API::Node subclassRef() {
2653-
result = API::moduleImport("logging").getMember("Logger").getASubclass*()
2654-
}
2655-
2656-
/** Gets a reference to an instance of `logging.Logger` or any subclass. */
2657-
API::Node instance() {
2658-
result = subclassRef().getReturn()
2659-
or
2660-
result = API::moduleImport("logging").getMember("root")
2661-
or
2662-
result = API::moduleImport("logging").getMember("getLogger").getReturn()
2663-
}
2664-
}
2665-
26662693
/**
26672694
* A call to one of the logging methods from `logging` or on a `logging.Logger`
26682695
* subclass.
@@ -2683,14 +2710,14 @@ private module StdlibPrivate {
26832710
method = "log" and
26842711
msgIndex = 1
26852712
|
2686-
this = Logger::instance().getMember(method).getACall()
2713+
this.(DataFlow::MethodCallNode).calls(Stdlib::Logger::instance(), method)
26872714
or
26882715
this = API::moduleImport("logging").getMember(method).getACall()
26892716
)
26902717
}
26912718

26922719
override DataFlow::Node getAnInput() {
2693-
result = this.getArgByName("msg")
2720+
result = this.getArgByName(["msg", "extra"])
26942721
or
26952722
result = this.getArg(any(int i | i >= msgIndex))
26962723
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Provides a taint-tracking configuration for tracking untrusted user input used in log entries.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `LogInjection::Configuration` is needed, otherwise
6+
* `LogInjectionCustomizations` should be imported instead.
7+
*/
8+
9+
import python
10+
import semmle.python.dataflow.new.DataFlow
11+
import semmle.python.dataflow.new.TaintTracking
12+
13+
/**
14+
* Provides a taint-tracking configuration for tracking untrusted user input used in log entries.
15+
*/
16+
module LogInjection {
17+
import LogInjectionCustomizations::LogInjection
18+
19+
/**
20+
* A taint-tracking configuration for tracking untrusted user input used in log entries.
21+
*/
22+
class Configuration extends TaintTracking::Configuration {
23+
Configuration() { this = "LogInjection" }
24+
25+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
26+
27+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
28+
29+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
30+
31+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
32+
guard instanceof SanitizerGuard
33+
}
34+
}
35+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* "log injection"
4+
* vulnerabilities, as well as extension points for adding your own.
5+
*/
6+
7+
private import python
8+
private import semmle.python.dataflow.new.DataFlow
9+
private import semmle.python.Concepts
10+
private import semmle.python.dataflow.new.RemoteFlowSources
11+
private import semmle.python.dataflow.new.BarrierGuards
12+
13+
/**
14+
* Provides default sources, sinks and sanitizers for detecting
15+
* "log injection"
16+
* vulnerabilities, as well as extension points for adding your own.
17+
*/
18+
module LogInjection {
19+
/**
20+
* A data flow source for "log injection" vulnerabilities.
21+
*/
22+
abstract class Source extends DataFlow::Node { }
23+
24+
/**
25+
* A data flow sink for "log injection" vulnerabilities.
26+
*/
27+
abstract class Sink extends DataFlow::Node { }
28+
29+
/**
30+
* A sanitizer for "log injection" vulnerabilities.
31+
*/
32+
abstract class Sanitizer extends DataFlow::Node { }
33+
34+
/**
35+
* A sanitizer guard for "log injection" vulnerabilities.
36+
*/
37+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
38+
39+
/**
40+
* A source of remote user input, considered as a flow source.
41+
*/
42+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
43+
44+
/**
45+
* A logging operation, considered as a flow sink.
46+
*/
47+
class LoggingAsSink extends Sink {
48+
LoggingAsSink() { this = any(Logging write).getAnInput() }
49+
}
50+
51+
/**
52+
* A comparison with a constant string, considered as a sanitizer-guard.
53+
*/
54+
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
55+
56+
/**
57+
* A call to replace line breaks, considered as a sanitizer.
58+
*/
59+
class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode {
60+
// Note: This sanitizer is not 100% accurate, since:
61+
// - we do not check that all kinds of line breaks are replaced
62+
// - we do not check that one kind of line breaks is not replaced by another
63+
//
64+
// However, we lack a simple way to do better, and the query would likely
65+
// be too noisy without this.
66+
//
67+
// TODO: Consider rewriting using flow states.
68+
ReplaceLineBreaksSanitizer() {
69+
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "replace" and
70+
this.getArg(0).asExpr().(StrConst).getText() in ["\r\n", "\n"]
71+
}
72+
}
73+
}

python/ql/src/experimental/Security/CWE-117/LogInjection.qhelp renamed to python/ql/src/Security/CWE-117/LogInjection.qhelp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
<p>If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.</p>
1010

11-
<p>Forgery can occur if a user provides some input creating the appearance of multiple
12-
log entries. This can include unescaped new-line characters, or HTML or other markup.</p>
11+
<p>Forgery can occur if a user provides some input with characters that are interpreted
12+
when the log output is displayed. If the log is displayed as a plain text file, then new
13+
line characters can be used by a malicious user to create the appearance of multiple log
14+
entries. If the log is displayed as HTML, then arbitrary HTML may be included to spoof
15+
log entries.</p>
1316
</overview>
1417

1518
<recommendation>
@@ -29,14 +32,14 @@ other forms of HTML injection.
2932

3033
<example>
3134
<p>
32-
In the example, the name provided by the user is recorded using the log output function (<code>logging.info</code> or <code>app.logger.info</code>, etc.).
33-
In these four cases, the name provided by the user is not provided The processing is recorded. If a malicious user provides <code>Guest%0D%0AUser name: Admin</code>
35+
In the example, the name provided by the user is recorded using the log output function (<code>logging.info</code> or <code>app.logger.info</code>, etc.).
36+
In these four cases, the name provided by the user is not provided The processing is recorded. If a malicious user provides <code>Guest%0D%0AUser name: Admin</code>
3437
as a parameter, the log entry will be divided into two lines, the first line is <code>User name: Guest</code> code>, the second line is <code>User name: Admin</code>.
3538
</p>
3639
<sample src="LogInjectionBad.py" />
3740

3841
<p>
39-
In a good example, the program uses the <code>replace</code> function to provide parameter processing to the user, and replace <code>\r\n</code> and <code>\n</code>
42+
In a good example, the program uses the <code>replace</code> function to provide parameter processing to the user, and replace <code>\r\n</code> and <code>\n</code>
4043
with empty characters. To a certain extent, the occurrence of log injection vulnerabilities is reduced.
4144
</p>
4245

python/ql/src/experimental/Security/CWE-117/LogInjection.ql renamed to python/ql/src/Security/CWE-117/LogInjection.ql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
* insertion of forged log entries by a malicious user.
55
* @kind path-problem
66
* @problem.severity error
7-
* @precision high
7+
* @security-severity 7.8
8+
* @precision medium
89
* @id py/log-injection
910
* @tags security
1011
* external/cwe/cwe-117
1112
*/
1213

1314
import python
14-
import experimental.semmle.python.security.injection.LogInjection
15+
import semmle.python.security.dataflow.LogInjection
1516
import DataFlow::PathGraph
1617

17-
from LogInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
18+
from LogInjection::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
1819
where config.hasFlowPath(source, sink)
1920
select sink.getNode(), source, sink, "$@ flows to log entry.", source.getNode(),
2021
"User-provided value"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* The query "Log Injection" (`py/log-injection`) has been promoted from experimental to the main query pack. Its results will now appear when `security-extended` is used. This query was originally [submitted as an experimental query by @haby0](https://github.com/github/codeql/pull/6182).

0 commit comments

Comments
 (0)