Ruby: add stacktrace exposure query

This commit is contained in:
Nick Rolfe
2022-07-29 17:27:16 +01:00
parent dd525a4f9b
commit b39e2ef71c
8 changed files with 196 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
/**
* Provides default sources, sinks and sanitizers for detecting stack trace
* exposure vulnerabilities, as well as extension points for adding your own.
*/
private import codeql.ruby.AST
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.frameworks.core.Kernel
/**
* Provides default sources, sinks and sanitizers for detecting stack trace
* exposure vulnerabilities, as well as extension points for adding your own.
*/
module StackTraceExposure {
/** A data flow source for stack trace exposure vulnerabilities. */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for stack trace exposure vulnerabilities. */
abstract class Sink extends DataFlow::Node { }
/** A data flow sanitizer for stack trace exposure vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/**
* A call to `backtrace` or `backtrace_locations` on a `rescue` variable,
* considered as a flow source.
*/
class BacktraceCall extends Source, DataFlow::CallNode {
BacktraceCall() {
exists(DataFlow::LocalSourceNode varAccess |
varAccess.asExpr().(ExprNodes::VariableReadAccessCfgNode).getExpr().getVariable() =
any(RescueClause rc).getVariableExpr().(VariableAccess).getVariable() and
varAccess.flowsTo(this.getReceiver())
) and
this.getMethodName() = ["backtrace", "backtrace_locations"]
}
}
/**
* A call to `Kernel#caller`, considered as a flow source.
*/
class KernelCallerCall extends Source, Kernel::KernelMethodCall {
KernelCallerCall() { this.getMethodName() = "caller" }
}
/**
* The body of an HTTP response that will be returned from a server,
* considered as a flow sink.
*/
class ServerHttpResponseBodyAsSink extends Sink {
ServerHttpResponseBodyAsSink() { this = any(Http::Server::HttpResponse response).getBody() }
}
}

View File

@@ -0,0 +1,25 @@
/**
* Provides a taint-tracking configuration for detecting stack-trace exposure
* vulnerabilities.
*
* Note, for performance reasons: only import this file if
* `StackTraceExposure::Configuration` is needed; otherwise,
* `StackTraceExposureCustomizations` should be imported instead.
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
private import StackTraceExposureCustomizations::StackTraceExposure
/**
* A taint-tracking configuration for detecting "stack trace exposure" vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "StackTraceExposure" }
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 }
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Software developers often add stack traces to error messages, as a debugging
aid. Whenever that error message occurs for an end user, the developer can use
the stack trace to help identify how to fix the problem. In particular, stack
traces can tell the developer more about the sequence of events that led to a
failure, as opposed to merely the final state of the software when the error
occurred.
</p>
<p>
Unfortunately, the same information can be useful to an attacker. The sequence
of class or method names in a stack trace can reveal the structure of the
application as well as any internal components it relies on. Furthermore, the
error message at the top of a stack trace can include information such as
server-side file names and SQL code that the application relies on, allowing an
attacker to fine-tune a subsequent injection attack.
</p>
</overview>
<recommendation>
<p>
Send the user a more generic error message that reveals less information.
Either suppress the stack trace entirely, or log it only on the server.
</p>
</recommendation>
<example>
<p>
In the following example, an exception is handled in two different ways. In the
first version, labeled BAD, the exception is exposted to the remote user by
rendering it as an HTTP response. As such, the user is able to see a detailed
stack trace, which may contain sensitive information. In the second version, the
error message is logged only on the server, and a generic error message is
displayed to the user. That way, the developers can still access and use the
error log, but remote users will not see the information. </p>
<sample src="examples/StackTraceExposure.rb" />
</example>
<references>
<li>OWASP: <a href="https://owasp.org/www-community/Improper_Error_Handling">Improper Error Handling</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,23 @@
/**
* @name Information exposure through an exception
* @description Leaking information about an exception, such as messages and stack traces, to an
* external user can expose implementation details that are useful to an attacker for
* developing a subsequent exploit.
* @kind path-problem
* @problem.severity error
* @security-severity 5.4
* @precision high
* @id rb/stack-trace-exposure
* @tags security
* external/cwe/cwe-209
* external/cwe/cwe-497
*/
import codeql.ruby.DataFlow
import codeql.ruby.security.StackTraceExposureQuery
import DataFlow::PathGraph
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ can be exposed to an external user.", source.getNode(),
"Error information"

View File

@@ -0,0 +1,18 @@
class UsersController < ApplicationController
def update_bad(id)
do_computation()
rescue => e
# BAD
render e.backtrace, content_type: "text/plain"
end
def update_good(id)
do_computation()
rescue => e
# GOOD
log e.backtrace
redner "Computation failed", content_type: "text/plain"
end
end

View File

@@ -0,0 +1,10 @@
edges
| StackTraceExposure.rb:11:10:11:17 | call to caller : | StackTraceExposure.rb:12:12:12:13 | bt |
nodes
| StackTraceExposure.rb:6:12:6:22 | call to backtrace | semmle.label | call to backtrace |
| StackTraceExposure.rb:11:10:11:17 | call to caller : | semmle.label | call to caller : |
| StackTraceExposure.rb:12:12:12:13 | bt | semmle.label | bt |
subpaths
#select
| StackTraceExposure.rb:6:12:6:22 | call to backtrace | StackTraceExposure.rb:6:12:6:22 | call to backtrace | StackTraceExposure.rb:6:12:6:22 | call to backtrace | $@ can be exposed to an external user. | StackTraceExposure.rb:6:12:6:22 | call to backtrace | Error information |
| StackTraceExposure.rb:12:12:12:13 | bt | StackTraceExposure.rb:11:10:11:17 | call to caller : | StackTraceExposure.rb:12:12:12:13 | bt | $@ can be exposed to an external user. | StackTraceExposure.rb:11:10:11:17 | call to caller | Error information |

View File

@@ -0,0 +1 @@
queries/security/cwe-209/StackTraceExposure.ql

View File

@@ -0,0 +1,15 @@
class FooController < ApplicationController
def show
something_that_might_fail()
rescue => e
render e.backtrace, content_type: "text/plain"
end
def show2
bt = caller()
render bt, content_type: "text/plain"
end
end