Merge pull request #4751 from erik-krogh/logInjection

Approved by asgerf, mchammer01
This commit is contained in:
CodeQL CI
2021-01-14 00:32:46 -08:00
committed by GitHub
13 changed files with 179 additions and 174 deletions

View File

@@ -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>

View File

@@ -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)

View File

@@ -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', () => {});

View File

@@ -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', () => {});

View File

@@ -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}/`);
});

View File

@@ -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}/`);
});

View File

@@ -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() }

View File

@@ -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() }
}
/**