-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[javascript] CodeQL query to detect Log Injection #3734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
41c0295
Add CodeQL query to detect Log Injection in JS code
dellalibera b4f2551
Update javascript/ql/src/experimental/Security/CWE-117/LogInjection.help
dellalibera cc91026
Update javascript/ql/src/experimental/Security/CWE-117/LogInjection.qll
dellalibera a759905
Update javascript/ql/src/experimental/Security/CWE-117/LogInjection.qll
dellalibera 65eba02
Merge remote-tracking branch 'upstream/master' into loginjection
dellalibera d9a0dc0
Remove check for console().getAMethodCall
dellalibera File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
javascript/ql/src/experimental/Security/CWE-117/LogInjection.help
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
|
||
<overview> | ||
|
||
<p>If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.</p> | ||
|
||
<p>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. If the log is displayed as HTML, then | ||
arbitrary HTML may be included to spoof log entries.</p> | ||
</overview> | ||
|
||
<recommendation> | ||
<p> | ||
User input should be suitably sanitized before it is logged. | ||
</p> | ||
<p> | ||
If the log entries are plain text then line breaks should be removed from user input, using | ||
<code>String.prototype.replace</code> or similar. Care should also be taken that user input is clearly marked | ||
in log entries, and that a malicious user cannot cause confusion in other ways. | ||
</p> | ||
<p> | ||
For log entries that will be displayed in HTML, user input should be HTML encoded before being logged, to prevent forgery and | ||
other forms of HTML injection. | ||
</p> | ||
|
||
</recommendation> | ||
|
||
<example> | ||
<p>In the first example, a username, provided by the user, is logged using `console.info`. In | ||
the first case, it is logged without any sanitization. In the second case the username is used to build an error that is logged using `console.error`. | ||
If a malicious user provides `username=Guest%0a[INFO]+User:+Admin%0a` as a username parameter, | ||
the log entry will be splitted in two different lines, where the second line will be `[INFO]+User:+Admin`. | ||
</p> | ||
<sample src="examples/logInjectionBad.js" /> | ||
|
||
<p> In the second example, <code>String.prototype.replace</code> is used to ensure no line endings are present in the user input.</p> | ||
<sample src="examples/logInjectionGood.js" /> | ||
</example> | ||
|
||
<references> | ||
<li>OWASP: <a href="https://www.owasp.org/index.php/Log_Injection">Log Injection</a>.</li> | ||
</references> | ||
</qhelp> |
20 changes: 20 additions & 0 deletions
20
javascript/ql/src/experimental/Security/CWE-117/LogInjection.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* @name Log Injection | ||
* @description Building log entries from user-controlled sources is vulnerable to | ||
* insertion of forged log entries by a malicious user. | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @precision high | ||
* @id js/log-injection | ||
* @tags security | ||
* external/cwe/cwe-117 | ||
*/ | ||
|
||
import javascript | ||
import DataFlow::PathGraph | ||
import LogInjection::LogInjection | ||
|
||
from LogInjectionConfiguration 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" |
99 changes: 99 additions & 0 deletions
99
javascript/ql/src/experimental/Security/CWE-117/LogInjection.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/** | ||
* Provides a taint-tracking configuration for reasoning about untrusted user input used in log entries. | ||
*/ | ||
|
||
import javascript | ||
|
||
module LogInjection { | ||
/** | ||
* A data flow source for user input used in log entries. | ||
*/ | ||
abstract class Source extends DataFlow::Node { } | ||
|
||
/** | ||
* A data flow sink for user input used in log entries. | ||
*/ | ||
abstract class Sink extends DataFlow::Node { } | ||
|
||
/** | ||
* A sanitizer for malicious user input used in log entries. | ||
*/ | ||
abstract class Sanitizer extends DataFlow::Node { } | ||
|
||
/** | ||
* A taint-tracking configuration for untrusted user input used in log entries. | ||
*/ | ||
class LogInjectionConfiguration extends TaintTracking::Configuration { | ||
LogInjectionConfiguration() { this = "LogInjection" } | ||
|
||
override predicate isSource(DataFlow::Node source) { source instanceof Source } | ||
|
||
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } | ||
|
||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer } | ||
} | ||
|
||
/** | ||
* A source of remote user controlled input. | ||
*/ | ||
class RemoteSource extends Source { | ||
RemoteSource() { this instanceof RemoteFlowSource } | ||
} | ||
|
||
/** | ||
* An source node representing a logging mechanism. | ||
*/ | ||
class ConsoleSource extends DataFlow::SourceNode { | ||
ConsoleSource() { | ||
exists(DataFlow::SourceNode node | | ||
node = this and this = DataFlow::moduleImport("console") | ||
or | ||
this = DataFlow::globalVarRef("console") | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* A call to a logging mechanism. For example, the call could be in the following forms: | ||
* `console.log('hello')` or | ||
* | ||
* `let logger = console.log;` | ||
* `logger('hello')` or | ||
* | ||
* `let logger = {info: console.log};` | ||
* `logger.info('hello')` | ||
*/ | ||
class LoggingCall extends DataFlow::CallNode { | ||
LoggingCall() { | ||
exists(DataFlow::SourceNode node, string propName | | ||
any(ConsoleSource console).getAPropertyRead() = node.getAPropertySource(propName) and | ||
this = node.getAPropertyRead(propName).getACall() | ||
) | ||
or | ||
this = any(LoggerCall call) | ||
} | ||
} | ||
|
||
/** | ||
* An argument to a logging mechanism. | ||
*/ | ||
class LoggingSink extends Sink { | ||
LoggingSink() { this = any(LoggingCall console).getAnArgument() } | ||
} | ||
|
||
/** | ||
* A call to `String.prototype.replace` that replaces `\n` is considered to sanitize the replaced string (reduce false positive). | ||
*/ | ||
class StringReplaceSanitizer extends Sanitizer { | ||
StringReplaceSanitizer() { | ||
exists(string s | this.(StringReplaceCall).replaces(s, "") and s.regexpMatch("\\n")) | ||
} | ||
} | ||
|
||
/** | ||
* A call to an HTML sanitizer is considered to sanitize the user input. | ||
*/ | ||
class HtmlSanitizer extends Sanitizer { | ||
HtmlSanitizer() { this instanceof HtmlSanitizerCall } | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
javascript/ql/src/experimental/Security/CWE-117/examples/logInjectionBad.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
const http = require('http'); | ||
const hostname = '127.0.0.1'; | ||
const port = 3000; | ||
const url = require('url'); | ||
|
||
|
||
const check_username = (username) => { | ||
if (username != 'name') throw `${username} is not valid`; | ||
// do something | ||
} | ||
|
||
const my_logger = { | ||
log: console.log | ||
} | ||
|
||
const another_logger = console.log | ||
|
||
// http://127.0.0.1:3000/data?username=Guest%0a[INFO]+User:+Admin%0a | ||
|
||
|
||
|
||
const server = http.createServer((req, res) => { | ||
let q = url.parse(req.url, true); | ||
|
||
let username = q.query.username; | ||
|
||
// BAD: User input logged as-is | ||
console.info(`[INFO] User: ${username}`); | ||
// [INFO] User: Guest | ||
// [INFO] User: Admin | ||
// | ||
|
||
// BAD: User input logged as-is | ||
console.info(`[INFO] User: %s`, username); | ||
// [INFO] User: Guest | ||
// [INFO] User: Admin | ||
// | ||
|
||
|
||
// BAD: User input logged as-is | ||
my_logger.log('[INFO] User:', username); | ||
// [INFO] User: Guest | ||
// [INFO] User: Admin | ||
// | ||
|
||
// BAD: User input logged as-is | ||
another_logger('[INFO] User:', username); | ||
// [INFO] User: Guest | ||
// [INFO] User: Admin | ||
// | ||
|
||
try { | ||
check_username(username) | ||
|
||
} catch (error) { | ||
// BAD: Error with user input logged as-is | ||
console.error(`[ERROR] Error: "${error}"`); | ||
// [ERROR] Error: "Guest | ||
// [INFO] User: Admin | ||
// is not valid" | ||
|
||
} | ||
|
||
}) | ||
|
||
server.listen(port, hostname, () => { | ||
console.log(`Server running at http://${hostname}:${port}/`); | ||
}); |
51 changes: 51 additions & 0 deletions
51
javascript/ql/src/experimental/Security/CWE-117/examples/logInjectionGood.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
const http = require('http'); | ||
const hostname = '127.0.0.1'; | ||
const port = 3000; | ||
const url = require('url'); | ||
|
||
const check_username = (username) => { | ||
if (username != 'name') throw `${username} is not valid`; | ||
// do something | ||
} | ||
|
||
const logger = { | ||
log: console.log | ||
} | ||
|
||
const another_logger = console.log | ||
|
||
// http://127.0.0.1:3000/data?username=Guest%0a[INFO]+User:+Admin%0a | ||
|
||
const server = http.createServer((req, res) => { | ||
let q = url.parse(req.url, true); | ||
|
||
// GOOD: remove `\n` line from user controlled input before logging | ||
let username = q.query.username.replace(/\n/g, ""); | ||
|
||
console.info(`[INFO] User: ${username}`); | ||
// [INFO] User: Guest[INFO] User: Admin | ||
|
||
console.info(`[INFO] User: %s`, username); | ||
// [INFO] User: Guest[INFO] User: Admin | ||
|
||
logger.log('[INFO] User:', username); | ||
// [INFO] User: Guest[INFO] User: Admin | ||
|
||
another_logger('[INFO] User:', username); | ||
// [INFO] User: Guest[INFO] User: Admin | ||
|
||
try { | ||
check_username(username) | ||
|
||
} catch (error) { | ||
console.error(`[ERROR] Error: "${error}"`); | ||
// [ERROR] Error: "Guest[INFO] User: Admin is not valid" | ||
|
||
} | ||
|
||
}) | ||
|
||
server.listen(port, hostname, () => { | ||
console.log(`Server running at http://${hostname}:${port}/`); | ||
}); | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly a comment.
This last conjunct should cover the two cases above if
codeql/javascript/ql/src/semmle/javascript/frameworks/Logging.qll
Line 57 in 7a5aae7
console().getAMemberCall(name)
.(I think
console.log
used to require a binding withFunction.prototype.bind
in order for it to work when invoked as a non-method call, but that appears to have changed now).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your review and feedback.
Yes, if it would be
console().getAMemberCall(name)
I could avoid the first casethis = any(ConsoleSource console).getAMemberCall(getAStandardLoggerMethodName())
but not the second one.
The second check
detects cases where the
console.log
(for example) is assigned to a property of another object.For example, consider the following case:
Though the
Logging.dll
will be updated toconsole().getAMemberCall(name)
, I will still miss the above case (please, do not hesitate to correct me if I am wrong).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have opened #3767 to address the first case, so lets drop that case in this PR.
For the second case, we have a bit better machinery for tracking special values of interest through properties and even calls ("type tracking"), but lets just keep the second case as is.
Let me know if you want to try out the "type tracking" feature later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the late reply and thank you again for your feedback.
I am really happy that I inspire the other PR. Since now it is merged, I remove the first case, and leave as is the second one.
Let me know if there is something else I can do.
About the "type tracking" feature is something that I will investigate.