From 20d54543fd9e45012b970e30f1de6d6d32c9aef8 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 25 Jan 2022 10:15:15 +0100 Subject: [PATCH 01/17] python: move log injection out of experimental - move from custom concept `LogOutput` to standard concept `Logging` - remove `Log.qll` from experimental frameworks - fold models into standard models (naively for now) - stdlib: - make Logger module public - broaden definition of instance - add `extra` keyword as possible source - flak: add app.logger as logger instance - django: `add django.utils.log.request_logger` as logger instance (should we add the rest?) - remove LogOutput from experimental concepts --- .../lib/semmle/python/frameworks/Django.qll | 13 ++ .../ql/lib/semmle/python/frameworks/Flask.qll | 11 ++ .../lib/semmle/python/frameworks/Stdlib.qll | 63 ++++++---- .../security/dataflow}/LogInjection.qll | 4 +- .../Security/CWE-117/LogInjection.qhelp | 0 .../Security/CWE-117/LogInjection.ql | 2 +- .../Security/CWE-117/LogInjectionBad.py | 0 .../Security/CWE-117/LogInjectionGood.py | 0 .../experimental/semmle/python/Concepts.qll | 30 ----- .../experimental/semmle/python/Frameworks.qll | 1 - .../semmle/python/frameworks/Log.qll | 118 ------------------ .../Security/CWE-117/LogInjection.qlref | 1 - .../LogInjection.expected | 0 .../CWE-117-LogInjection/LogInjection.qlref | 1 + .../CWE-117-LogInjection}/LogInjectionBad.py | 0 .../CWE-117-LogInjection}/LogInjectionGood.py | 0 16 files changed, 66 insertions(+), 178 deletions(-) rename python/ql/{src/experimental/semmle/python/security/injection => lib/semmle/python/security/dataflow}/LogInjection.qll (91%) rename python/ql/src/{experimental => }/Security/CWE-117/LogInjection.qhelp (100%) rename python/ql/src/{experimental => }/Security/CWE-117/LogInjection.ql (90%) rename python/ql/src/{experimental => }/Security/CWE-117/LogInjectionBad.py (100%) rename python/ql/src/{experimental => }/Security/CWE-117/LogInjectionGood.py (100%) delete mode 100644 python/ql/src/experimental/semmle/python/frameworks/Log.qll delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.qlref rename python/ql/test/{experimental/query-tests/Security/CWE-117 => query-tests/Security/CWE-117-LogInjection}/LogInjection.expected (100%) create mode 100644 python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.qlref rename python/ql/test/{experimental/query-tests/Security/CWE-117 => query-tests/Security/CWE-117-LogInjection}/LogInjectionBad.py (100%) rename python/ql/test/{experimental/query-tests/Security/CWE-117 => query-tests/Security/CWE-117-LogInjection}/LogInjectionGood.py (100%) diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index 9e66c728f6ee..a79c3ba0c0de 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -2295,4 +2295,17 @@ module PrivateDjango { override string getMimetypeDefault() { none() } } + + // --------------------------------------------------------------------------- + // Logging + // --------------------------------------------------------------------------- + /** + * Django provides a standard Python logger. + */ + private class DjangoLogger extends Stdlib::Logger::LoggerInstance { + DjangoLogger() { + this = + API::moduleImport("django").getMember("utils").getMember("log").getMember("request_logger") + } + } } diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 88544ca85378..0db46d06e10a 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import semmle.python.Concepts private import semmle.python.frameworks.Werkzeug +private import semmle.python.frameworks.Stdlib private import semmle.python.ApiGraphs private import semmle.python.frameworks.internal.InstanceTaintStepsHelper private import semmle.python.security.dataflow.PathInjectionCustomizations @@ -569,4 +570,14 @@ module Flask { result in [this.getArg(0), this.getArgByName("filename_or_fp")] } } + + // --------------------------------------------------------------------------- + // Logging + // --------------------------------------------------------------------------- + /** + * The attribute `logger` on a Flask application is a standard Python logger. + */ + private class FlaskLogger extends Stdlib::Logger::LoggerInstance { + FlaskLogger() { this = FlaskApp::instance().getMember("logger") } + } } diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index dae470aee87c..742786b76578 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -237,6 +237,42 @@ module Stdlib { } } } + + // --------------------------------------------------------------------------- + // logging + // --------------------------------------------------------------------------- + /** + * Provides models for the `logging.Logger` class and subclasses. + * + * See https://docs.python.org/3.9/library/logging.html#logging.Logger. + */ + module Logger { + /** + * An instance of `logging.Logger`. Extend this class to model new instances. + * Most major frameworks will provide a logger instance as a class attribute. + */ + abstract class LoggerInstance extends API::Node { + override string toString() { result = "logger" } + } + + /** Gets a reference to the `logging.Logger` class or any subclass. */ + API::Node subclassRef() { + result = API::moduleImport("logging").getMember("Logger").getASubclass*() + } + + /** Gets a reference to an instance of `logging.Logger` or any subclass. */ + API::Node instance() { + result instanceof LoggerInstance + or + result = subclassRef().getReturn() + or + result = API::moduleImport("logging") + or + result = API::moduleImport("logging").getMember("root") + or + result = API::moduleImport("logging").getMember("getLogger").getReturn() + } + } } /** @@ -2642,27 +2678,6 @@ private module StdlibPrivate { // --------------------------------------------------------------------------- // logging // --------------------------------------------------------------------------- - /** - * Provides models for the `logging.Logger` class and subclasses. - * - * See https://docs.python.org/3.9/library/logging.html#logging.Logger. - */ - module Logger { - /** Gets a reference to the `logging.Logger` class or any subclass. */ - API::Node subclassRef() { - result = API::moduleImport("logging").getMember("Logger").getASubclass*() - } - - /** Gets a reference to an instance of `logging.Logger` or any subclass. */ - API::Node instance() { - result = subclassRef().getReturn() - or - result = API::moduleImport("logging").getMember("root") - or - result = API::moduleImport("logging").getMember("getLogger").getReturn() - } - } - /** * A call to one of the logging methods from `logging` or on a `logging.Logger` * subclass. @@ -2683,14 +2698,12 @@ private module StdlibPrivate { method = "log" and msgIndex = 1 | - this = Logger::instance().getMember(method).getACall() - or - this = API::moduleImport("logging").getMember(method).getACall() + this = Stdlib::Logger::instance().getMember(method).getACall() ) } override DataFlow::Node getAnInput() { - result = this.getArgByName("msg") + result = this.getArgByName(["msg", "extra"]) or result = this.getArg(any(int i | i >= msgIndex)) } diff --git a/python/ql/src/experimental/semmle/python/security/injection/LogInjection.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll similarity index 91% rename from python/ql/src/experimental/semmle/python/security/injection/LogInjection.qll rename to python/ql/lib/semmle/python/security/dataflow/LogInjection.qll index 4ee9c69a4a91..682926c21ecb 100644 --- a/python/ql/src/experimental/semmle/python/security/injection/LogInjection.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll @@ -1,6 +1,6 @@ import python import semmle.python.Concepts -import experimental.semmle.python.Concepts +import semmle.python.Concepts import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.RemoteFlowSources @@ -13,7 +13,7 @@ class LogInjectionFlowConfig extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { sink = any(LogOutput logoutput).getAnInput() } + override predicate isSink(DataFlow::Node sink) { sink = any(Logging write).getAnInput() } override predicate isSanitizer(DataFlow::Node node) { exists(CallNode call | diff --git a/python/ql/src/experimental/Security/CWE-117/LogInjection.qhelp b/python/ql/src/Security/CWE-117/LogInjection.qhelp similarity index 100% rename from python/ql/src/experimental/Security/CWE-117/LogInjection.qhelp rename to python/ql/src/Security/CWE-117/LogInjection.qhelp diff --git a/python/ql/src/experimental/Security/CWE-117/LogInjection.ql b/python/ql/src/Security/CWE-117/LogInjection.ql similarity index 90% rename from python/ql/src/experimental/Security/CWE-117/LogInjection.ql rename to python/ql/src/Security/CWE-117/LogInjection.ql index 452ef9448251..f768fbb8d097 100644 --- a/python/ql/src/experimental/Security/CWE-117/LogInjection.ql +++ b/python/ql/src/Security/CWE-117/LogInjection.ql @@ -11,7 +11,7 @@ */ import python -import experimental.semmle.python.security.injection.LogInjection +import semmle.python.security.dataflow.LogInjection import DataFlow::PathGraph from LogInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink diff --git a/python/ql/src/experimental/Security/CWE-117/LogInjectionBad.py b/python/ql/src/Security/CWE-117/LogInjectionBad.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-117/LogInjectionBad.py rename to python/ql/src/Security/CWE-117/LogInjectionBad.py diff --git a/python/ql/src/experimental/Security/CWE-117/LogInjectionGood.py b/python/ql/src/Security/CWE-117/LogInjectionGood.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-117/LogInjectionGood.py rename to python/ql/src/Security/CWE-117/LogInjectionGood.py diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 3b1f6072f0c2..bb83fbda5838 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -14,36 +14,6 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks -/** Provides classes for modeling log related APIs. */ -module LogOutput { - /** - * A data flow node for log output. - * - * Extend this class to model new APIs. If you want to refine existing API models, - * extend `LogOutput` instead. - */ - abstract class Range extends DataFlow::Node { - /** - * Get the parameter value of the log output function. - */ - abstract DataFlow::Node getAnInput(); - } -} - -/** - * A data flow node for log output. - * - * Extend this class to refine existing API models. If you want to model new APIs, - * extend `LogOutput::Range` instead. - */ -class LogOutput extends DataFlow::Node { - LogOutput::Range range; - - LogOutput() { this = range } - - DataFlow::Node getAnInput() { result = range.getAnInput() } -} - /** Provides classes for modeling LDAP query execution-related APIs. */ module LDAPQuery { /** diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 90fe21cc9331..f438852e3dc5 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -8,7 +8,6 @@ private import experimental.semmle.python.frameworks.Django private import experimental.semmle.python.frameworks.Werkzeug private import experimental.semmle.python.frameworks.LDAP private import experimental.semmle.python.frameworks.NoSQL -private import experimental.semmle.python.frameworks.Log private import experimental.semmle.python.frameworks.JWT private import experimental.semmle.python.libraries.PyJWT private import experimental.semmle.python.libraries.Authlib diff --git a/python/ql/src/experimental/semmle/python/frameworks/Log.qll b/python/ql/src/experimental/semmle/python/frameworks/Log.qll deleted file mode 100644 index 675b9be1c2d3..000000000000 --- a/python/ql/src/experimental/semmle/python/frameworks/Log.qll +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Provides classes modeling security-relevant aspects of the log libraries. - */ - -private import python -private import semmle.python.dataflow.new.DataFlow -private import semmle.python.dataflow.new.TaintTracking -private import semmle.python.dataflow.new.RemoteFlowSources -private import experimental.semmle.python.Concepts -private import semmle.python.frameworks.Flask -private import semmle.python.ApiGraphs - -/** - * Provides models for Python's log-related libraries. - */ -private module log { - /** - * Log output method list. - * - * See https://docs.python.org/3/library/logging.html#logger-objects - */ - private class LogOutputMethods extends string { - LogOutputMethods() { - this in ["info", "error", "warn", "warning", "debug", "critical", "exception", "log"] - } - } - - /** - * The class used to find the log output method of the `logging` module. - * - * See `LogOutputMethods` - */ - private class LoggingCall extends DataFlow::CallCfgNode, LogOutput::Range { - LoggingCall() { - this = API::moduleImport("logging").getMember(any(LogOutputMethods m)).getACall() - } - - override DataFlow::Node getAnInput() { - this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and - result in [this.getArg(_), this.getArgByName(_)] // this includes the arg named "msg" - or - this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and - result in [this.getArg(any(int i | i > 0)), this.getArgByName(any(string s | s != "level"))] - } - } - - /** - * The class used to find log output methods related to the `logging.getLogger` instance. - * - * See `LogOutputMethods` - */ - private class LoggerCall extends DataFlow::CallCfgNode, LogOutput::Range { - LoggerCall() { - this = - API::moduleImport("logging") - .getMember("getLogger") - .getReturn() - .getMember(any(LogOutputMethods m)) - .getACall() - } - - override DataFlow::Node getAnInput() { - this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and - result in [this.getArg(_), this.getArgByName(_)] // this includes the arg named "msg" - or - this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and - result in [this.getArg(any(int i | i > 0)), this.getArgByName(any(string s | s != "level"))] - } - } - - /** - * The class used to find the relevant log output method of the `flask.Flask.logger` instance (flask application). - * - * See `LogOutputMethods` - */ - private class FlaskLoggingCall extends DataFlow::CallCfgNode, LogOutput::Range { - FlaskLoggingCall() { - this = - Flask::FlaskApp::instance() - .getMember("logger") - .getMember(any(LogOutputMethods m)) - .getACall() - } - - override DataFlow::Node getAnInput() { - this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and - result in [this.getArg(_), this.getArgByName(_)] // this includes the arg named "msg" - or - this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and - result in [this.getArg(any(int i | i > 0)), this.getArgByName(any(string s | s != "level"))] - } - } - - /** - * The class used to find the relevant log output method of the `django.utils.log.request_logger` instance (django application). - * - * See `LogOutputMethods` - */ - private class DjangoLoggingCall extends DataFlow::CallCfgNode, LogOutput::Range { - DjangoLoggingCall() { - this = - API::moduleImport("django") - .getMember("utils") - .getMember("log") - .getMember("request_logger") - .getMember(any(LogOutputMethods m)) - .getACall() - } - - override DataFlow::Node getAnInput() { - this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and - result in [this.getArg(_), this.getArgByName(_)] // this includes the arg named "msg" - or - this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and - result in [this.getArg(any(int i | i > 0)), this.getArgByName(any(string s | s != "level"))] - } - } -} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.qlref deleted file mode 100644 index 021cc357ac28..000000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE-117/LogInjection.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.expected b/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.expected similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.expected rename to python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.expected diff --git a/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.qlref b/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.qlref new file mode 100644 index 000000000000..1837c628c33e --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjection.qlref @@ -0,0 +1 @@ +Security/CWE-117/LogInjection.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjectionBad.py b/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjectionBad.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-117/LogInjectionBad.py rename to python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjectionBad.py diff --git a/python/ql/test/experimental/query-tests/Security/CWE-117/LogInjectionGood.py b/python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjectionGood.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-117/LogInjectionGood.py rename to python/ql/test/query-tests/Security/CWE-117-LogInjection/LogInjectionGood.py From 8b5114d10e12321b1a08c8a36550844e1b46fcaa Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 25 Jan 2022 11:04:18 +0100 Subject: [PATCH 02/17] python: Add standard customization setup - modernize the sanitizer, but do not make it less specific --- .../python/security/dataflow/LogInjection.qll | 37 +++++++---- .../dataflow/LogInjectionCustomizations.qll | 65 +++++++++++++++++++ .../ql/src/Security/CWE-117/LogInjection.ql | 2 +- 3 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll index 682926c21ecb..1e9d0b7a99f9 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjection.qll @@ -1,24 +1,35 @@ +/** + * Provides a taint-tracking configuration for tracking untrusted user input used in log entries. + * + * Note, for performance reasons: only import this file if + * `LogInjection::Configuration` is needed, otherwise + * `LogInjectionCustomizations` should be imported instead. + */ + import python -import semmle.python.Concepts -import semmle.python.Concepts import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking -import semmle.python.dataflow.new.RemoteFlowSources /** - * A taint-tracking configuration for tracking untrusted user input used in log entries. + * Provides a taint-tracking configuration for tracking untrusted user input used in log entries. */ -class LogInjectionFlowConfig extends TaintTracking::Configuration { - LogInjectionFlowConfig() { this = "LogInjectionFlowConfig" } +module LogInjection { + import LogInjectionCustomizations::LogInjection + + /** + * A taint-tracking configuration for tracking untrusted user input used in log entries. + */ + class Configuration extends TaintTracking::Configuration { + Configuration() { this = "LogInjection" } + + override predicate isSource(DataFlow::Node source) { source instanceof Source } - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } - override predicate isSink(DataFlow::Node sink) { sink = any(Logging write).getAnInput() } + override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer } - override predicate isSanitizer(DataFlow::Node node) { - exists(CallNode call | - node.asCfgNode() = call.getFunction().(AttrNode).getObject("replace") and - call.getArg(0).getNode().(StrConst).getText() in ["\r\n", "\n"] - ) + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof SanitizerGuard + } } } diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll new file mode 100644 index 000000000000..ef54c985f7e3 --- /dev/null +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -0,0 +1,65 @@ +/** + * Provides default sources, sinks and sanitizers for detecting + * "command injection" + * vulnerabilities, as well as extension points for adding your own. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.Concepts +private import semmle.python.dataflow.new.RemoteFlowSources +private import semmle.python.dataflow.new.BarrierGuards + +/** + * Provides default sources, sinks and sanitizers for detecting + * "command injection" + * vulnerabilities, as well as extension points for adding your own. + */ +module LogInjection { + /** + * A data flow source for "command injection" vulnerabilities. + */ + abstract class Source extends DataFlow::Node { } + + /** + * A data flow sink for "command injection" vulnerabilities. + */ + abstract class Sink extends DataFlow::Node { } + + /** + * A sanitizer for "command injection" vulnerabilities. + */ + abstract class Sanitizer extends DataFlow::Node { } + + /** + * A sanitizer guard for "command injection" vulnerabilities. + */ + abstract class SanitizerGuard extends DataFlow::BarrierGuard { } + + /** + * A source of remote user input, considered as a flow source. + */ + class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { } + + /** + * A logging operation, considered as a flow sink. + */ + class LoggingAsSink extends Sink { + LoggingAsSink() { this = any(Logging write).getAnInput() } + } + + /** + * A comparison with a constant string, considered as a sanitizer-guard. + */ + class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { } + + /** + * A call to replace line breaks functions as a sanitizer. + */ + class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode { + ReplaceLineBreaksSanitizer() { + this.getFunction().(DataFlow::AttrRead).getAttributeName() = "replace" and + this.getArg(0).asExpr().(StrConst).getText() in ["\r\n", "\n"] + } + } +} diff --git a/python/ql/src/Security/CWE-117/LogInjection.ql b/python/ql/src/Security/CWE-117/LogInjection.ql index f768fbb8d097..18549f8dd889 100644 --- a/python/ql/src/Security/CWE-117/LogInjection.ql +++ b/python/ql/src/Security/CWE-117/LogInjection.ql @@ -14,7 +14,7 @@ import python import semmle.python.security.dataflow.LogInjection import DataFlow::PathGraph -from LogInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +from LogInjection::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) select sink.getNode(), source, sink, "$@ flows to log entry.", source.getNode(), "User-provided value" From bf1145ece0c877b83ccb2e37a139c6028d19154f Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 25 Jan 2022 16:51:48 +0100 Subject: [PATCH 03/17] python: Add change note should we have the `lgtm,codescanning` handshake or not? --- python/old-change-notes/2022-02-25-promote-log-injection.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 python/old-change-notes/2022-02-25-promote-log-injection.md diff --git a/python/old-change-notes/2022-02-25-promote-log-injection.md b/python/old-change-notes/2022-02-25-promote-log-injection.md new file mode 100644 index 000000000000..ea9afd663c2a --- /dev/null +++ b/python/old-change-notes/2022-02-25-promote-log-injection.md @@ -0,0 +1,2 @@ +lgtm,codescanning +* The query "Log Injection" (`py/log-injection`) has been promoted from experimental to the main query pack. Its results will now appear by default. This query was originally [submitted as an experimental query by @haby0](https://github.com/github/codeql/pull/6182). From 9d416664a1576b79cc76f3fd3dcc4de81dbf91db Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 25 Jan 2022 17:15:54 +0100 Subject: [PATCH 04/17] python: modern change note I set the category to newQuery since that is what users will see. When we have tags, it would be nice to tag it as a query promotion. --- .../src/change-notes}/2022-02-25-promote-log-injection.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename python/{old-change-notes => ql/src/change-notes}/2022-02-25-promote-log-injection.md (90%) diff --git a/python/old-change-notes/2022-02-25-promote-log-injection.md b/python/ql/src/change-notes/2022-02-25-promote-log-injection.md similarity index 90% rename from python/old-change-notes/2022-02-25-promote-log-injection.md rename to python/ql/src/change-notes/2022-02-25-promote-log-injection.md index ea9afd663c2a..c87d633e0071 100644 --- a/python/old-change-notes/2022-02-25-promote-log-injection.md +++ b/python/ql/src/change-notes/2022-02-25-promote-log-injection.md @@ -1,2 +1,4 @@ -lgtm,codescanning +--- +category: newQuery +--- * The query "Log Injection" (`py/log-injection`) has been promoted from experimental to the main query pack. Its results will now appear by default. This query was originally [submitted as an experimental query by @haby0](https://github.com/github/codeql/pull/6182). From c03f89d71222aee154ff2623cfa065049f362ad2 Mon Sep 17 00:00:00 2001 From: yoff Date: Tue, 1 Feb 2022 10:04:26 +0100 Subject: [PATCH 05/17] Apply suggestions from code review Co-authored-by: Rasmus Wriedt Larsen --- python/ql/lib/semmle/python/frameworks/Django.qll | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index a79c3ba0c0de..d90058e073b3 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -2300,7 +2300,8 @@ module PrivateDjango { // Logging // --------------------------------------------------------------------------- /** - * Django provides a standard Python logger. + * A standard Python logger instance from Django. + * see https://github.com/django/django/blob/stable/4.0.x/django/utils/log.py#L11 */ private class DjangoLogger extends Stdlib::Logger::LoggerInstance { DjangoLogger() { From 7511b335124a85a2e4d67d837e792d9a70de8afe Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 10:23:16 +0100 Subject: [PATCH 06/17] python: "command" -> "log" --- .../security/dataflow/LogInjectionCustomizations.qll | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll index ef54c985f7e3..ad5841d6a0e0 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -1,6 +1,6 @@ /** * Provides default sources, sinks and sanitizers for detecting - * "command injection" + * "log injection" * vulnerabilities, as well as extension points for adding your own. */ @@ -12,27 +12,27 @@ private import semmle.python.dataflow.new.BarrierGuards /** * Provides default sources, sinks and sanitizers for detecting - * "command injection" + * "log injection" * vulnerabilities, as well as extension points for adding your own. */ module LogInjection { /** - * A data flow source for "command injection" vulnerabilities. + * A data flow source for "log injection" vulnerabilities. */ abstract class Source extends DataFlow::Node { } /** - * A data flow sink for "command injection" vulnerabilities. + * A data flow sink for "log injection" vulnerabilities. */ abstract class Sink extends DataFlow::Node { } /** - * A sanitizer for "command injection" vulnerabilities. + * A sanitizer for "log injection" vulnerabilities. */ abstract class Sanitizer extends DataFlow::Node { } /** - * A sanitizer guard for "command injection" vulnerabilities. + * A sanitizer guard for "log injection" vulnerabilities. */ abstract class SanitizerGuard extends DataFlow::BarrierGuard { } From 26befebfc274527ffc9d8a4451b68c2353e2963c Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 10:34:36 +0100 Subject: [PATCH 07/17] python: drop precision and add severity score Given both the original FP score and our concerns regarding sanitizers, `@precision medium`, which is aligned with other languages, feels appropriate. --- python/ql/src/Security/CWE-117/LogInjection.ql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Security/CWE-117/LogInjection.ql b/python/ql/src/Security/CWE-117/LogInjection.ql index 18549f8dd889..d29dee55fbdd 100644 --- a/python/ql/src/Security/CWE-117/LogInjection.ql +++ b/python/ql/src/Security/CWE-117/LogInjection.ql @@ -4,7 +4,8 @@ * insertion of forged log entries by a malicious user. * @kind path-problem * @problem.severity error - * @precision high + * @security-severity 7.8 + * @precision medium * @id py/log-injection * @tags security * external/cwe/cwe-117 From ecea392a086553f6c76ab058284bcf81c6e7d03e Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 10:47:18 +0100 Subject: [PATCH 08/17] python: rewrite qhelp overview (combining the Java version and the JS version) --- python/ql/src/Security/CWE-117/LogInjection.qhelp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/python/ql/src/Security/CWE-117/LogInjection.qhelp b/python/ql/src/Security/CWE-117/LogInjection.qhelp index e5305220997f..5d112806f86e 100644 --- a/python/ql/src/Security/CWE-117/LogInjection.qhelp +++ b/python/ql/src/Security/CWE-117/LogInjection.qhelp @@ -8,8 +8,11 @@

If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.

-

Forgery can occur if a user provides some input creating the appearance of multiple - log entries. This can include unescaped new-line characters, or HTML or other markup.

+

Forgery can occur if a user provides some input with characters that are interpreted +when the log output is displayed. If the log is displayed as a plain text file, then new +line characters can be used by a malicious user to create the appearance of multiple log +entries. If the log is displayed as HTML, then arbitrary HTML may be included to spoof +log entries.

@@ -29,14 +32,14 @@ other forms of HTML injection.

-In the example, the name provided by the user is recorded using the log output function (logging.info or app.logger.info, etc.). -In these four cases, the name provided by the user is not provided The processing is recorded. If a malicious user provides Guest%0D%0AUser name: Admin +In the example, the name provided by the user is recorded using the log output function (logging.info or app.logger.info, etc.). +In these four cases, the name provided by the user is not provided The processing is recorded. If a malicious user provides Guest%0D%0AUser name: Admin as a parameter, the log entry will be divided into two lines, the first line is User name: Guest code>, the second line is User name: Admin.

-In a good example, the program uses the replace function to provide parameter processing to the user, and replace \r\n and \n +In a good example, the program uses the replace function to provide parameter processing to the user, and replace \r\n and \n with empty characters. To a certain extent, the occurrence of log injection vulnerabilities is reduced.

From 119a7e4f34c1c59e6544cff1e6e78de254ff44b6 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 10:55:45 +0100 Subject: [PATCH 09/17] python: provide links for Flask --- python/ql/lib/semmle/python/frameworks/Flask.qll | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 0db46d06e10a..5f7674c907eb 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -575,7 +575,11 @@ module Flask { // Logging // --------------------------------------------------------------------------- /** - * The attribute `logger` on a Flask application is a standard Python logger. + * A Flask application provides a standard Python logger via the `logger` attribute. + * + * See + * - https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.logger + * - https://flask.palletsprojects.com/en/2.0.x/logging/ */ private class FlaskLogger extends Stdlib::Logger::LoggerInstance { FlaskLogger() { this = FlaskApp::instance().getMember("logger") } From c587084758e27bf65aeef110367653a4a6cbf7f7 Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 13:31:16 +0100 Subject: [PATCH 10/17] python: use standard `InstanceSource` construction --- .../lib/semmle/python/frameworks/Django.qll | 8 ++- .../ql/lib/semmle/python/frameworks/Flask.qll | 4 +- .../lib/semmle/python/frameworks/Stdlib.qll | 54 ++++++++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index d90058e073b3..8f34043f0937 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -2303,10 +2303,14 @@ module PrivateDjango { * A standard Python logger instance from Django. * see https://github.com/django/django/blob/stable/4.0.x/django/utils/log.py#L11 */ - private class DjangoLogger extends Stdlib::Logger::LoggerInstance { + private class DjangoLogger extends Stdlib::Logger::InstanceSource { DjangoLogger() { this = - API::moduleImport("django").getMember("utils").getMember("log").getMember("request_logger") + API::moduleImport("django") + .getMember("utils") + .getMember("log") + .getMember("request_logger") + .getAnImmediateUse() } } } diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 5f7674c907eb..7f54761d911d 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -581,7 +581,7 @@ module Flask { * - https://flask.palletsprojects.com/en/2.0.x/api/#flask.Flask.logger * - https://flask.palletsprojects.com/en/2.0.x/logging/ */ - private class FlaskLogger extends Stdlib::Logger::LoggerInstance { - FlaskLogger() { this = FlaskApp::instance().getMember("logger") } + private class FlaskLogger extends Stdlib::Logger::InstanceSource { + FlaskLogger() { this = FlaskApp::instance().getMember("logger").getAnImmediateUse() } } } diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index 742786b76578..f11b96a77ed7 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -242,36 +242,48 @@ module Stdlib { // logging // --------------------------------------------------------------------------- /** - * Provides models for the `logging.Logger` class and subclasses. + * Provides models for the `logging.Logger` class * * See https://docs.python.org/3.9/library/logging.html#logging.Logger. */ module Logger { + /** Gets a reference to the `logging.Logger` class or any subclass. */ + private API::Node subclassRef() { + result = API::moduleImport("logging").getMember("Logger").getASubclass*() + } + /** - * An instance of `logging.Logger`. Extend this class to model new instances. - * Most major frameworks will provide a logger instance as a class attribute. + * A source of instances of `logging.Logger`, extend this class to model new instances. + * + * This can include instantiations of the class, return values from function + * calls, or a special parameter that will be set when functions are called by an external + * library. + * + * Use the predicate `Logger::instance()` to get references to instances of `logging.Logger`. */ - abstract class LoggerInstance extends API::Node { - override string toString() { result = "logger" } - } + abstract class InstanceSource extends DataFlow::LocalSourceNode { } - /** Gets a reference to the `logging.Logger` class or any subclass. */ - API::Node subclassRef() { - result = API::moduleImport("logging").getMember("Logger").getASubclass*() + /** A direct instantiation of `logging.Logger`. */ + private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode { + ClassInstantiation() { + this = subclassRef().getACall() + or + this = API::moduleImport("logging").getMember("root").getAnImmediateUse() + or + this = API::moduleImport("logging").getMember("getLogger").getACall() + } } - /** Gets a reference to an instance of `logging.Logger` or any subclass. */ - API::Node instance() { - result instanceof LoggerInstance - or - result = subclassRef().getReturn() - or - result = API::moduleImport("logging") - or - result = API::moduleImport("logging").getMember("root") + /** Gets a reference to an instance of `logging.Logger`. */ + private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) { + t.start() and + result instanceof InstanceSource or - result = API::moduleImport("logging").getMember("getLogger").getReturn() + exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t)) } + + /** Gets a reference to an instance of `logging.Logger`. */ + DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) } } } @@ -2698,7 +2710,9 @@ private module StdlibPrivate { method = "log" and msgIndex = 1 | - this = Stdlib::Logger::instance().getMember(method).getACall() + this.(DataFlow::MethodCallNode).calls(Stdlib::Logger::instance(), method) + or + this = API::moduleImport("logging").getMember(method).getACall() ) } From bec8c0daea57068579f0d8c5062993a5c4f1bbeb Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Tue, 1 Feb 2022 13:39:03 +0100 Subject: [PATCH 11/17] python: update change note --- python/ql/src/change-notes/2022-02-25-promote-log-injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/change-notes/2022-02-25-promote-log-injection.md b/python/ql/src/change-notes/2022-02-25-promote-log-injection.md index c87d633e0071..79d3aa23ab72 100644 --- a/python/ql/src/change-notes/2022-02-25-promote-log-injection.md +++ b/python/ql/src/change-notes/2022-02-25-promote-log-injection.md @@ -1,4 +1,4 @@ --- category: newQuery --- -* The query "Log Injection" (`py/log-injection`) has been promoted from experimental to the main query pack. Its results will now appear by default. This query was originally [submitted as an experimental query by @haby0](https://github.com/github/codeql/pull/6182). +* 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). From 448e0785c201373411e0895db13d6fa4cd6b590a Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Wed, 2 Feb 2022 09:04:16 +0100 Subject: [PATCH 12/17] python: `logging.root` is not a call --- python/ql/lib/semmle/python/frameworks/Stdlib.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index f11b96a77ed7..11c19c6c43fc 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -264,7 +264,7 @@ module Stdlib { abstract class InstanceSource extends DataFlow::LocalSourceNode { } /** A direct instantiation of `logging.Logger`. */ - private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode { + private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode { ClassInstantiation() { this = subclassRef().getACall() or From f21ac04285da82ebfc8648491393529f07b5d17a Mon Sep 17 00:00:00 2001 From: yoff Date: Wed, 9 Feb 2022 09:22:31 +0100 Subject: [PATCH 13/17] Update python/ql/lib/semmle/python/frameworks/Stdlib.qll Co-authored-by: Rasmus Wriedt Larsen --- python/ql/lib/semmle/python/frameworks/Stdlib.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index 11c19c6c43fc..ef02cfa8d770 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -242,7 +242,7 @@ module Stdlib { // logging // --------------------------------------------------------------------------- /** - * Provides models for the `logging.Logger` class + * Provides models for the `logging.Logger` class and subclasses. * * See https://docs.python.org/3.9/library/logging.html#logging.Logger. */ From bd14adefa05a102b5011fd5d3b480edef6c4c19c Mon Sep 17 00:00:00 2001 From: Rasmus Lerchedahl Petersen Date: Mon, 14 Feb 2022 11:37:46 +0100 Subject: [PATCH 14/17] python: add apologetic comment --- .../python/security/dataflow/LogInjectionCustomizations.qll | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll index ad5841d6a0e0..ae5386528c04 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -57,6 +57,11 @@ module LogInjection { * A call to replace line breaks functions as a sanitizer. */ class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode { + // This is actually not safe: + // - we do not check that all kinds of line breaks are replaced + // - we do not check that one kind of line breaks is not replaced by another + // however, we lack a simple way to do better, and the query would likely + // be too noisy without this. Consider rewriting using flow states. ReplaceLineBreaksSanitizer() { this.getFunction().(DataFlow::AttrRead).getAttributeName() = "replace" and this.getArg(0).asExpr().(StrConst).getText() in ["\r\n", "\n"] From 62598c0fd143af5763b61ce82477cd3897f957e1 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 14 Feb 2022 16:07:40 +0100 Subject: [PATCH 15/17] Update python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll Co-authored-by: Rasmus Wriedt Larsen --- .../python/security/dataflow/LogInjectionCustomizations.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll index ae5386528c04..457ed73599ed 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -54,7 +54,7 @@ module LogInjection { class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { } /** - * A call to replace line breaks functions as a sanitizer. + * A call to replace line breaks, considered as a sanitizer. */ class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode { // This is actually not safe: From 3a995ec1b1189a0c585fcde54f131114ad26d195 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 14 Feb 2022 16:08:44 +0100 Subject: [PATCH 16/17] Update python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll Co-authored-by: Rasmus Wriedt Larsen --- .../security/dataflow/LogInjectionCustomizations.qll | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll index 457ed73599ed..badb7386eeb6 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -57,11 +57,14 @@ module LogInjection { * A call to replace line breaks, considered as a sanitizer. */ class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode { - // This is actually not safe: + // Note: This sanitizer is not 100% accurate, since: // - we do not check that all kinds of line breaks are replaced // - we do not check that one kind of line breaks is not replaced by another - // however, we lack a simple way to do better, and the query would likely - // be too noisy without this. Consider rewriting using flow states. + // + // However, we lack a simple way to do better, and the query would likely + // be too noisy without this. + // + // TODO: Consider rewriting using flow states. ReplaceLineBreaksSanitizer() { this.getFunction().(DataFlow::AttrRead).getAttributeName() = "replace" and this.getArg(0).asExpr().(StrConst).getText() in ["\r\n", "\n"] From 62d4bb50a53a7450dfb57265ffa7af29afc3ab73 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Tue, 15 Feb 2022 10:38:52 +0100 Subject: [PATCH 17/17] Python: Autoformat Trailing whitespace is a bit too easy with the ```suggestions through the UI :| --- .../python/security/dataflow/LogInjectionCustomizations.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll index badb7386eeb6..c564951b8d87 100644 --- a/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/LogInjectionCustomizations.qll @@ -62,7 +62,7 @@ module LogInjection { // - we do not check that one kind of line breaks is not replaced by another // // However, we lack a simple way to do better, and the query would likely - // be too noisy without this. + // be too noisy without this. // // TODO: Consider rewriting using flow states. ReplaceLineBreaksSanitizer() {