Ruby: Add rb/tainted-format-string query

This commit is contained in:
Harry Maclean
2022-02-28 15:59:45 +13:00
parent 4bf35ad188
commit f6215d4c7e
9 changed files with 270 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* format injections, as well as extension points for adding your own.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.ApiGraphs
module TaintedFormatString {
/**
* A data flow source for format injections.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for format injections.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for format injections.
*/
abstract class Sanitizer extends DataFlow::Node { }
/** A source of remote user input, considered as a flow source for format injection. */
class RemoteSource extends Source {
RemoteSource() { this instanceof RemoteFlowSource }
}
/**
* A format argument to a printf-like function, considered as a flow sink for format injection.
*/
class FormatSink extends Sink {
FormatSink() {
exists(PrintfCall printf |
this = printf.getFormatString() and
// exclude trivial case where there are no arguments to interpolate
exists(printf.getFormatArgument(_))
)
}
}
/**
* A call to `printf` or `sprintf`.
*/
abstract class PrintfCall extends DataFlow::CallNode {
// We assume that most printf-like calls have the signature f(format_string, args...)
DataFlow::Node getFormatString() { result = this.getArgument(0) }
DataFlow::Node getFormatArgument(int n) { n > 0 and result = this.getArgument(n) }
}
class KernelPrintfCall extends PrintfCall {
KernelPrintfCall() {
this = API::getTopLevelMember("Kernel").getAMethodCall("printf")
or
this.asExpr().getExpr() instanceof UnknownMethodCall and
this.getMethodName() = "printf"
}
// Kernel#printf supports two signatures:
// printf(io, string, ...)
// printf(string, ...)
override DataFlow::Node getFormatString() { result = this.getArgument([0, 1]) }
}
class KernelSprintfCall extends PrintfCall {
KernelSprintfCall() {
this = API::getTopLevelMember("Kernel").getAMethodCall("sprintf")
or
this.asExpr().getExpr() instanceof UnknownMethodCall and
this.getMethodName() = "sprintf"
}
}
class IOPrintfCall extends PrintfCall {
IOPrintfCall() { this = API::getTopLevelMember("IO").getInstance().getAMethodCall("printf") }
}
}

View File

@@ -0,0 +1,30 @@
/**
* Provides a taint-tracking configuration for reasoning about format
* injections.
*
*
* Note, for performance reasons: only import this file if
* `TaintedFormatString::Configuration` is needed, otherwise
* `TaintedFormatStringCustomizations` should be imported instead.
*/
import ruby
import codeql.ruby.DataFlow
import codeql.ruby.TaintTracking
import TaintedFormatStringCustomizations::TaintedFormatString
/**
* A taint-tracking configuration for format injections.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "TaintedFormatString" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof Sanitizer
}
}

View File

@@ -0,0 +1,51 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Methods like <code>Kernel.printf</code> accept a format string that is used to format
the remaining arguments by providing inline format specifiers. If the format string
contains unsanitized input from an untrusted source, then that string may contain
unexpected format specifiers that cause garbled output or throw an exception.
</p>
</overview>
<recommendation>
<p>
Either sanitize the input before including it in the format string, or use a
<code>%s</code> specifier in the format string, and pass the untrusted data as corresponding
argument.
</p>
</recommendation>
<example>
<p>
The following program snippet logs information about an unauthorized access attempt. The
log message includes the user name, and the user's IP address is passed as an additional
argument to <code>Kernel.printf</code> to be appended to the message:
</p>
<sample src="examples/tainted_format_string_bad.rb"/>
<p>
However, if a malicious user provides a format specified such as <code>%s</code> as their
user name, <code>Kernel.printf</code> throw an exception that there are too few arguments
to satisfy the format. This can result in denial of service or leaking of internal
information to the attacker via a stack trace.
</p>
<p>
Instead, the user name should be included using the <code>%s</code> specifier:
</p>
<sample src="examples/tainted_format_string_good.rb"/>
<p>
Alternatively, a method such as <code>Kernel.puts</code> should be used, which does not
apply string formatting to its arguments.
</p>
</example>
<references>
<li>Ruby documentation for <a href="https://docs.ruby-lang.org/en/3.1/Kernel.html#method-i-sprintf">format strings</a>.</li>
<li>Common Weakness Enumeration: <a href="https://cwe.mitre.org/data/definitions/134.html">CWE-134</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,20 @@
/**
* @name Use of externally-controlled format string
* @description Using external input in format strings can lead to garbled output.
* @kind path-problem
* @problem.severity warning
* @security-severity 7.3
* @precision high
* @id rb/tainted-format-string
* @tags security
* external/cwe/cwe-134
*/
import ruby
import codeql.ruby.security.TaintedFormatStringQuery
import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows here and is used in a format string.",
source.getNode(), "User-provided value"

View File

@@ -0,0 +1,5 @@
class UsersController < ActionController::Base
def index
printf("Unauthorised access attempt by #{params[:user]}: %s", request.ip)
end
end

View File

@@ -0,0 +1,5 @@
class UsersController < ActionController::Base
def index
printf("Unauthorised access attempt by %s: %s", params[:user], request.ip)
end
end

View File

@@ -0,0 +1,46 @@
edges
| tainted_format_string.rb:4:12:4:17 | call to params : | tainted_format_string.rb:4:12:4:26 | ...[...] |
| tainted_format_string.rb:5:19:5:24 | call to params : | tainted_format_string.rb:5:19:5:33 | ...[...] |
| tainted_format_string.rb:10:23:10:28 | call to params : | tainted_format_string.rb:10:23:10:37 | ...[...] |
| tainted_format_string.rb:11:30:11:35 | call to params : | tainted_format_string.rb:11:30:11:44 | ...[...] |
| tainted_format_string.rb:13:23:13:28 | call to params : | tainted_format_string.rb:13:23:13:37 | ...[...] |
| tainted_format_string.rb:14:30:14:35 | call to params : | tainted_format_string.rb:14:30:14:44 | ...[...] |
| tainted_format_string.rb:16:27:16:32 | call to params : | tainted_format_string.rb:16:27:16:41 | ...[...] |
| tainted_format_string.rb:17:20:17:25 | call to params : | tainted_format_string.rb:17:20:17:34 | ...[...] |
| tainted_format_string.rb:23:19:23:24 | call to params : | tainted_format_string.rb:23:19:23:33 | ...[...] |
| tainted_format_string.rb:29:32:29:37 | call to params : | tainted_format_string.rb:29:32:29:46 | ...[...] : |
| tainted_format_string.rb:29:32:29:46 | ...[...] : | tainted_format_string.rb:29:12:29:46 | ... + ... |
nodes
| tainted_format_string.rb:4:12:4:17 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:4:12:4:26 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:5:19:5:24 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:5:19:5:33 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:10:23:10:28 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:10:23:10:37 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:11:30:11:35 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:11:30:11:44 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:13:23:13:28 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:13:23:13:37 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:14:30:14:35 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:14:30:14:44 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:16:27:16:32 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:16:27:16:41 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:17:20:17:25 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:17:20:17:34 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:23:19:23:24 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:23:19:23:33 | ...[...] | semmle.label | ...[...] |
| tainted_format_string.rb:29:12:29:46 | ... + ... | semmle.label | ... + ... |
| tainted_format_string.rb:29:32:29:37 | call to params : | semmle.label | call to params : |
| tainted_format_string.rb:29:32:29:46 | ...[...] : | semmle.label | ...[...] : |
subpaths
#select
| tainted_format_string.rb:4:12:4:26 | ...[...] | tainted_format_string.rb:4:12:4:17 | call to params : | tainted_format_string.rb:4:12:4:26 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:4:12:4:17 | call to params | User-provided value |
| tainted_format_string.rb:5:19:5:33 | ...[...] | tainted_format_string.rb:5:19:5:24 | call to params : | tainted_format_string.rb:5:19:5:33 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:5:19:5:24 | call to params | User-provided value |
| tainted_format_string.rb:10:23:10:37 | ...[...] | tainted_format_string.rb:10:23:10:28 | call to params : | tainted_format_string.rb:10:23:10:37 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:10:23:10:28 | call to params | User-provided value |
| tainted_format_string.rb:11:30:11:44 | ...[...] | tainted_format_string.rb:11:30:11:35 | call to params : | tainted_format_string.rb:11:30:11:44 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:11:30:11:35 | call to params | User-provided value |
| tainted_format_string.rb:13:23:13:37 | ...[...] | tainted_format_string.rb:13:23:13:28 | call to params : | tainted_format_string.rb:13:23:13:37 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:13:23:13:28 | call to params | User-provided value |
| tainted_format_string.rb:14:30:14:44 | ...[...] | tainted_format_string.rb:14:30:14:35 | call to params : | tainted_format_string.rb:14:30:14:44 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:14:30:14:35 | call to params | User-provided value |
| tainted_format_string.rb:16:27:16:41 | ...[...] | tainted_format_string.rb:16:27:16:32 | call to params : | tainted_format_string.rb:16:27:16:41 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:16:27:16:32 | call to params | User-provided value |
| tainted_format_string.rb:17:20:17:34 | ...[...] | tainted_format_string.rb:17:20:17:25 | call to params : | tainted_format_string.rb:17:20:17:34 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:17:20:17:25 | call to params | User-provided value |
| tainted_format_string.rb:23:19:23:33 | ...[...] | tainted_format_string.rb:23:19:23:24 | call to params : | tainted_format_string.rb:23:19:23:33 | ...[...] | $@ flows here and is used in a format string. | tainted_format_string.rb:23:19:23:24 | call to params | User-provided value |
| tainted_format_string.rb:29:12:29:46 | ... + ... | tainted_format_string.rb:29:32:29:37 | call to params : | tainted_format_string.rb:29:12:29:46 | ... + ... | $@ flows here and is used in a format string. | tainted_format_string.rb:29:32:29:37 | call to params | User-provided value |

View File

@@ -0,0 +1 @@
queries/security/cwe-134/TaintedFormatString.ql

View File

@@ -0,0 +1,31 @@
class UsersController < ActionController::Base
def show
printf(params[:format], arg) # BAD
Kernel.printf(params[:format], arg) # BAD
printf(params[:format]) # GOOD
Kernel.printf(params[:format]) # GOOD
printf(IO.new(1), params[:format], arg) # BAD
Kernel.printf(IO.new(1), params[:format], arg) # BAD
printf(IO.new(1), params[:format]) # GOOD [FALSE POSITIVE]
Kernel.printf(IO.new(1), params[:format]) # GOOD [FALSE POSITIVE]
str1 = Kernel.sprintf(params[:format], arg) # BAD
str2 = sprintf(params[:format], arg) # BAD
str1 = Kernel.sprintf(params[:format]) # GOOD
str2 = sprintf(params[:format]) # GOOD
stdout = IO.new 1
stdout.printf(params[:format], arg) # BAD
stdout.printf(params[:format]) # GOOD
# Taint via string concatenation
printf("A log message: " + params[:format], arg) # BAD
end
end