Ruby: Model ActionView helper XSS sinks

This commit is contained in:
Harry Maclean
2022-09-06 12:35:25 +01:00
parent a8197b27aa
commit ed0c85e3af
6 changed files with 145 additions and 0 deletions

View File

@@ -216,4 +216,116 @@ class FileSystemResolverAccess extends DataFlow::CallNode, FileSystemAccess::Ran
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}
// TODO: model flow in/out of template files properly,
//
/**
* Action view helper methods which are XSS sinks.
*/
module ActionViewHelpers {
/**
* Calls to ActionView helpers which render their argument without escaping.
* These arguments should be treated as XSS sinks.
* In the documentation for classes in this module, the vulnerable argument is
* named `x`.
*/
abstract class RawHelperCall extends MethodCall {
abstract Expr getRawArgument();
}
/**
* `ActionView::Helpers::TextHelper#simple_format`.
*
* `simple_format(x, y, sanitize: false)`.
*/
private class SimpleFormat extends ActionViewContextCall, RawHelperCall {
SimpleFormat() {
this.getMethodName() = "simple_format" and
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(0) }
}
/**
* `ActionView::Helpers::TextHelper#truncate`.
*
* `truncate(x, escape: false)`.
*/
private class Truncate extends ActionViewContextCall, RawHelperCall {
Truncate() {
this.getMethodName() = "truncate" and
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(0) }
}
/**
* `ActionView::Helpers::TextHelper#highlight`.
*
* `truncate(x, y, sanitize: false)`.
*/
private class Highlight extends ActionViewContextCall, RawHelperCall {
Highlight() {
this.getMethodName() = "highlight" and
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(0) }
}
/**
* `ActionView::Helpers::JavascriptHelper#javascript_tag`.
*
* `javascript_tag(x)`.
*/
private class JavascriptTag extends ActionViewContextCall, RawHelperCall {
JavascriptTag() { this.getMethodName() = "javascript_tag" }
override Expr getRawArgument() { result = this.getArgument(0) }
}
/**
* `ActionView::Helpers::TagHelper#tag`.
*
* `tag(x, x, y, false)`.
*/
private class ContentTag extends ActionViewContextCall, RawHelperCall {
ContentTag() {
this.getMethodName() = "content_tag" and
this.getArgument(3).getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(1) }
}
/**
* `ActionView::Helpers::TagHelper#tag`.
*
* `tag(x, x, y, false)`.
*/
private class Tag extends ActionViewContextCall, RawHelperCall {
Tag() {
this.getMethodName() = "tag" and
this.getArgument(3).getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(0) }
}
/**
* `ActionView::Helpers::TagHelper#tag.<tag name>`.
*
* `tag.h1(x, escape: false)`.
*/
private class TagMethod extends MethodCall, RawHelperCall {
TagMethod() {
inActionViewContext(this) and
this.getReceiver().(MethodCall).getMethodName() = "tag" and
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
}
override Expr getRawArgument() { result = this.getArgument(0) }
}
}

View File

@@ -75,6 +75,18 @@ private module Shared {
RawCallArgumentAsSink() { this.getCall() instanceof RawCall }
}
/**
* An argument to an ActionView helper method which is not escaped,
* considered as a flow sink.
*/
class RawHelperCallArgumentAsSink extends Sink, ErbOutputMethodCallArgumentNode {
RawHelperCallArgumentAsSink() {
exists(ErbOutputDirective d, ActionViewHelpers::RawHelperCall c |
d.getTerminalStmt() = c and this.asExpr().getExpr() = c.getRawArgument()
)
}
}
/**
* A argument to a call to the `link_to` method, which does not expect
* unsanitized user-input, considered as a flow sink.

View File

@@ -30,3 +30,12 @@ httpResponses
| app/controllers/foo/bars_controller.rb:36:12:36:67 | call to render_to_string | app/controllers/foo/bars_controller.rb:36:29:36:33 | @user | application/json |
| app/controllers/foo/bars_controller.rb:38:5:38:50 | call to render | app/controllers/foo/bars_controller.rb:38:12:38:22 | call to backtrace | text/plain |
| app/controllers/foo/bars_controller.rb:44:5:44:17 | call to render | app/controllers/foo/bars_controller.rb:44:12:44:17 | "show" | text/html |
rawHelperCalls
| action_view/helpers.erb:4:1:4:36 | call to simple_format | action_view/helpers.erb:4:15:4:15 | call to x |
| action_view/helpers.erb:7:1:7:26 | call to truncate | action_view/helpers.erb:7:10:7:10 | call to x |
| action_view/helpers.erb:10:1:10:29 | call to highlight | action_view/helpers.erb:10:11:10:11 | call to x |
| action_view/helpers.erb:12:1:12:17 | call to javascript_tag | action_view/helpers.erb:12:16:12:16 | call to x |
| action_view/helpers.erb:15:1:15:27 | call to content_tag | action_view/helpers.erb:15:16:15:16 | call to y |
| action_view/helpers.erb:18:1:18:19 | call to tag | action_view/helpers.erb:18:5:18:5 | call to x |
| action_view/helpers.erb:21:1:21:24 | call to h1 | action_view/helpers.erb:21:8:21:8 | call to x |
| action_view/helpers.erb:24:1:24:23 | call to p | action_view/helpers.erb:24:7:24:7 | call to x |

View File

@@ -1,3 +1,4 @@
private import ruby
private import codeql.ruby.frameworks.ActionController
private import codeql.ruby.frameworks.ActionView
private import codeql.ruby.Concepts
@@ -16,3 +17,7 @@ query predicate linkToCalls(LinkToCall c) { any() }
query predicate httpResponses(Http::Server::HttpResponse r, DataFlow::Node body, string mimeType) {
r.getBody() = body and r.getMimetype() = mimeType
}
query predicate rawHelperCalls(ActionViewHelpers::RawHelperCall c, Expr arg) {
arg = c.getRawArgument()
}

View File

@@ -22,6 +22,7 @@ edges
| app/views/foo/bars/show.html.erb:44:76:44:87 | call to display_text : | app/views/foo/bars/show.html.erb:44:64:44:87 | ... + ... : |
| app/views/foo/bars/show.html.erb:54:29:54:34 | call to params : | app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] |
| app/views/foo/bars/show.html.erb:57:13:57:18 | call to params : | app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] |
| app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] |
nodes
| app/controllers/foo/bars_controller.rb:9:12:9:17 | call to params : | semmle.label | call to params : |
| app/controllers/foo/bars_controller.rb:9:12:9:29 | ...[...] : | semmle.label | ...[...] : |
@@ -50,6 +51,8 @@ nodes
| app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] | semmle.label | ...[...] |
| app/views/foo/bars/show.html.erb:57:13:57:18 | call to params : | semmle.label | call to params : |
| app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | semmle.label | ...[...] |
| app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | semmle.label | call to params : |
| app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | semmle.label | ...[...] |
subpaths
#select
| app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | app/controllers/foo/bars_controller.rb:18:10:18:15 | call to params : | app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | Cross-site scripting vulnerability due to a $@. | app/controllers/foo/bars_controller.rb:18:10:18:15 | call to params | user-provided value |
@@ -64,3 +67,4 @@ subpaths
| app/views/foo/bars/show.html.erb:51:5:51:18 | call to user_name_memo | app/controllers/foo/bars_controller.rb:13:20:13:25 | call to params : | app/views/foo/bars/show.html.erb:51:5:51:18 | call to user_name_memo | Cross-site scripting vulnerability due to a $@. | app/controllers/foo/bars_controller.rb:13:20:13:25 | call to params | user-provided value |
| app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] | app/views/foo/bars/show.html.erb:54:29:54:34 | call to params : | app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:54:29:54:34 | call to params | user-provided value |
| app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | app/views/foo/bars/show.html.erb:57:13:57:18 | call to params : | app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:57:13:57:18 | call to params | user-provided value |
| app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:74:19:74:24 | call to params | user-provided value |

View File

@@ -69,3 +69,6 @@
html_escaped_in_template = h params[:text]
html_escaped_in_template.html_safe
%>
<%# BAD: simple_format called with sanitize: false %>
<%= simple_format(params[:comment], sanitize: false) %>