mirror of
https://github.com/github/codeql.git
synced 2026-04-29 02:35:15 +02:00
Ruby: Model ActionView helper XSS sinks
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
Reference in New Issue
Block a user