mirror of
https://github.com/github/codeql.git
synced 2026-04-26 17:25:19 +02:00
Merge pull request #4751 from erik-krogh/logInjection
Approved by asgerf, mchammer01
This commit is contained in:
@@ -18,12 +18,12 @@ arbitrary HTML may be included to spoof log entries.</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
|
||||
If the log entries are in 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.
|
||||
in log entries.
|
||||
</p>
|
||||
<p>
|
||||
For log entries that will be displayed in HTML, user input should be HTML encoded before being logged, to prevent forgery and
|
||||
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>
|
||||
|
||||
@@ -31,7 +31,7 @@ other forms of HTML injection.
|
||||
|
||||
<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`.
|
||||
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>
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @name Log Injection
|
||||
* @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
|
||||
* @precision medium
|
||||
* @id js/log-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-117
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import LogInjection::LogInjection
|
||||
import semmle.javascript.security.dataflow.LogInjection::LogInjection
|
||||
|
||||
from LogInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
@@ -0,0 +1,10 @@
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let q = url.parse(req.url, true);
|
||||
|
||||
console.info(`[INFO] User: ${q.query.username}`); // BAD: User input logged as-is
|
||||
})
|
||||
|
||||
server.listen(3000, '127.0.0.1', () => {});
|
||||
@@ -0,0 +1,13 @@
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
let q = url.parse(req.url, true);
|
||||
|
||||
// GOOD: remove newlines from user controlled input before logging
|
||||
let username = q.query.username.replace(/\n|\r/g, "");
|
||||
|
||||
console.info(`[INFO] User: ${username}`);
|
||||
});
|
||||
|
||||
server.listen(3000, '127.0.0.1', () => {});
|
||||
@@ -1,68 +0,0 @@
|
||||
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}/`);
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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}/`);
|
||||
});
|
||||
|
||||
@@ -38,11 +38,22 @@ string getAStandardLoggerMethodName() {
|
||||
*/
|
||||
private module Console {
|
||||
/**
|
||||
* Gets a data flow source node for the console library.
|
||||
* An API entrypoint for the global `console` variable.
|
||||
*/
|
||||
private DataFlow::SourceNode console() {
|
||||
result = DataFlow::moduleImport("console") or
|
||||
result = DataFlow::globalVarRef("console")
|
||||
private class ConsoleGlobalEntry extends API::EntryPoint {
|
||||
ConsoleGlobalEntry() { this = "ConsoleGlobalEntry" }
|
||||
|
||||
override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef("console") }
|
||||
|
||||
override DataFlow::Node getARhs() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a api node for the console library.
|
||||
*/
|
||||
private API::Node console() {
|
||||
result = API::moduleImport("console") or
|
||||
result = API::root().getASuccessor(any(ConsoleGlobalEntry e))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +67,7 @@ private module Console {
|
||||
name = getAStandardLoggerMethodName() or
|
||||
name = "assert"
|
||||
) and
|
||||
this = console().getAMemberCall(name)
|
||||
this = console().getMember(name).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAMessageComponent() {
|
||||
@@ -85,7 +96,7 @@ private module Loglevel {
|
||||
*/
|
||||
class LoglevelLoggerCall extends LoggerCall {
|
||||
LoglevelLoggerCall() {
|
||||
this = DataFlow::moduleMember("loglevel", getAStandardLoggerMethodName()).getACall()
|
||||
this = API::moduleImport("loglevel").getMember(getAStandardLoggerMethodName()).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAMessageComponent() { result = getAnArgument() }
|
||||
@@ -102,9 +113,11 @@ private module Winston {
|
||||
class WinstonLoggerCall extends LoggerCall, DataFlow::MethodCallNode {
|
||||
WinstonLoggerCall() {
|
||||
this =
|
||||
DataFlow::moduleMember("winston", "createLogger")
|
||||
API::moduleImport("winston")
|
||||
.getMember("createLogger")
|
||||
.getReturn()
|
||||
.getMember(getAStandardLoggerMethodName())
|
||||
.getACall()
|
||||
.getAMethodCall(getAStandardLoggerMethodName())
|
||||
}
|
||||
|
||||
override DataFlow::Node getAMessageComponent() {
|
||||
@@ -125,9 +138,11 @@ private module log4js {
|
||||
class Log4jsLoggerCall extends LoggerCall {
|
||||
Log4jsLoggerCall() {
|
||||
this =
|
||||
DataFlow::moduleMember("log4js", "getLogger")
|
||||
API::moduleImport("log4js")
|
||||
.getMember("getLogger")
|
||||
.getReturn()
|
||||
.getMember(getAStandardLoggerMethodName())
|
||||
.getACall()
|
||||
.getAMethodCall(getAStandardLoggerMethodName())
|
||||
}
|
||||
|
||||
override DataFlow::Node getAMessageComponent() { result = getAnArgument() }
|
||||
@@ -145,7 +160,7 @@ private module Npmlog {
|
||||
string name;
|
||||
|
||||
Npmlog() {
|
||||
this = DataFlow::moduleMember("npmlog", name).getACall() and
|
||||
this = API::moduleImport("npmlog").getMember(name).getACall() and
|
||||
name = getAStandardLoggerMethodName()
|
||||
}
|
||||
|
||||
@@ -170,8 +185,8 @@ private module Fancylog {
|
||||
*/
|
||||
class Fancylog extends LoggerCall {
|
||||
Fancylog() {
|
||||
this = DataFlow::moduleMember("fancy-log", getAStandardLoggerMethodName()).getACall() or
|
||||
this = DataFlow::moduleImport("fancy-log").getACall()
|
||||
this = API::moduleImport("fancy-log").getMember(getAStandardLoggerMethodName()).getACall() or
|
||||
this = API::moduleImport("fancy-log").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getAMessageComponent() { result = getAnArgument() }
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Provides default sources, sink, and sanitizers for reasoning about untrusted user input used in log entries.
|
||||
*/
|
||||
module LogInjection {
|
||||
/**
|
||||
* A data flow source for user input used in log entries.
|
||||
@@ -40,45 +43,11 @@ module LogInjection {
|
||||
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() }
|
||||
LoggingSink() { this = any(LoggerCall console).getAMessageComponent() }
|
||||
}
|
||||
|
||||
/**
|
||||
Reference in New Issue
Block a user