Merge pull request #12311 from maikypedia/maikypedia/ruby-ssti

Ruby: Add Server Side Template Injection query
This commit is contained in:
Alex Ford
2023-03-20 15:26:27 +00:00
committed by GitHub
16 changed files with 496 additions and 0 deletions

View File

@@ -1063,3 +1063,69 @@ module Cryptography {
class BlockMode = SC::BlockMode;
}
/**
* A data-flow node that constructs a template.
*
* Often, it is worthy of an alert if a template is constructed such that
* executing it would be a security risk.
*
* If it is important that the template is rendered, use `TemplateRendering`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `TemplateConstruction::Range` instead.
*/
class TemplateConstruction extends DataFlow::Node instanceof TemplateConstruction::Range {
/** Gets the argument that specifies the template to be constructed. */
DataFlow::Node getTemplate() { result = super.getTemplate() }
}
/** Provides a class for modeling new template rendering APIs. */
module TemplateConstruction {
/**
* A data-flow node that constructs a template.
*
* Often, it is worthy of an alert if a template is constructed such that
* executing it would be a security risk.
*
* If it is important that the template is rendered, use `TemplateRendering`.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `TemplateConstruction` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the template to be constructed. */
abstract DataFlow::Node getTemplate();
}
}
/**
* A data-flow node that renders templates.
*
* If the context of interest is such that merely constructing a template
* would be valuable to report, consider using `TemplateConstruction`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `TemplateRendering::Range` instead.
*/
class TemplateRendering extends DataFlow::Node instanceof TemplateRendering::Range {
/** Gets the argument that specifies the template to be rendered. */
DataFlow::Node getTemplate() { result = super.getTemplate() }
}
/** Provides a class for modeling new template rendering APIs. */
module TemplateRendering {
/**
* A data-flow node that renders templates.
*
* If the context of interest is such that merely constructing a template
* would be valuable to report, consider using `TemplateConstruction`.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `TemplateRendering` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the template to be rendered. */
abstract DataFlow::Node getTemplate();
}
}

View File

@@ -27,5 +27,7 @@ private import codeql.ruby.frameworks.ActionDispatch
private import codeql.ruby.frameworks.PosixSpawn
private import codeql.ruby.frameworks.StringFormatters
private import codeql.ruby.frameworks.Json
private import codeql.ruby.frameworks.Erb
private import codeql.ruby.frameworks.Slim
private import codeql.ruby.frameworks.Sinatra
private import codeql.ruby.frameworks.Twirp

View File

@@ -0,0 +1,46 @@
/**
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
*/
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.Concepts
/**
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
*/
module Erb {
/**
* Flow summary for `ERB.new`. This method wraps a template string, compiling it.
*/
private class TemplateSummary extends SummarizedCallable {
TemplateSummary() { this = "ERB.new" }
override MethodCall getACall() { result = any(ErbTemplateNewCall c).asExpr().getExpr() }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
}
}
/** A call to `ERB.new`, considered as a template construction. */
private class ErbTemplateNewCall extends TemplateConstruction::Range, DataFlow::CallNode {
ErbTemplateNewCall() { this = API::getTopLevelMember("ERB").getAnInstantiation() }
override DataFlow::Node getTemplate() { result = this.getArgument(0) }
}
/** A call to `ERB.new(foo).result(binding)`, considered as a template rendering. */
private class ErbTemplateRendering extends TemplateRendering::Range, DataFlow::CallNode {
private DataFlow::Node template;
ErbTemplateRendering() {
exists(ErbTemplateNewCall templateConstruction |
this = templateConstruction.getAMethodCall("result") and
template = templateConstruction.getTemplate()
)
}
override DataFlow::Node getTemplate() { result = template }
}
}

View File

@@ -0,0 +1,38 @@
/**
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
*/
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.Concepts
/**
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
*/
module Slim {
/** A call to `Slim::Template.new`, considered as a template construction. */
private class SlimTemplateNewCall extends TemplateConstruction::Range, DataFlow::CallNode {
SlimTemplateNewCall() {
this = API::getTopLevelMember("Slim").getMember("Template").getAnInstantiation()
}
override DataFlow::Node getTemplate() {
result.asExpr().getExpr() =
this.getBlock().(DataFlow::BlockNode).asCallableAstNode().getAStmt()
}
}
/** A call to `Slim::Template.new{ foo }.render`, considered as a template rendering */
private class SlimTemplateRendering extends TemplateRendering::Range, DataFlow::CallNode {
private DataFlow::Node template;
SlimTemplateRendering() {
exists(SlimTemplateNewCall templateConstruction |
this = templateConstruction.getAMethodCall("render") and
template = templateConstruction.getTemplate()
)
}
override DataFlow::Node getTemplate() { result = template }
}
}

View File

@@ -0,0 +1,49 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* ERB Server Side Template Injections, as well as extension points for adding your own
*/
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.RemoteFlowSources
/**
* Provides default sources, sinks and sanitizers for detecting
* Server Side Template Injections, as well as extension points for adding your own
*/
module TemplateInjection {
/** A data flow source for SSTI vulnerabilities */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for SSTI vulnerabilities */
abstract class Sink extends DataFlow::Node { }
/** A sanitizer for SSTI vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/**
* A source of remote user input, considered as a flow source.
*/
private class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
/**
* An Server Side Template Injection rendering, considered as a flow sink.
*/
private class TemplateRenderingAsSink extends Sink {
TemplateRenderingAsSink() { this = any(TemplateRendering e).getTemplate() }
}
/**
* A comparison with a constant string, considered as a sanitizer-guard.
*/
private class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
/**
* An inclusion check against an array of constant strings, considered as a
* sanitizer-guard.
*/
private class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
StringConstArrayInclusionCallBarrier
{ }
}

View File

@@ -0,0 +1,21 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* Server Side Template Injections, as well as extension points for adding your own
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
import TemplateInjectionCustomizations::TemplateInjection
/**
* A taint-tracking configuration for detecting Server Side Template Injections vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "TemplateInjection" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node source) { source instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
}

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new experimental query, `rb/server-side-template-injection`, to detect cases where user input may be embedded into a template's code in an unsafe manner.

View File

@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Template Injection occurs when user input is embedded in a template's code in an unsafe manner.
An attacker can use native template syntax to inject a malicious payload into a template, which is then executed server-side.
This permits the attacker to run arbitrary code in the server's context.
</p>
</overview>
<recommendation>
<p>
To fix this, ensure that untrusted input is not used as part of a template's code. If the application requirements do not allow this,
use a sandboxed environment where access to unsafe attributes and methods is prohibited.
</p>
</recommendation>
<example>
<p>
Consider the example given below, an untrusted HTTP parameter <code>name</code> is used to generate a template string. This can lead to remote code execution.
</p>
<sample src="examples/SSTIBad.rb" />
<p>
Here we have fixed the problem by including ERB/Slim syntax in the string, then the user input will be rendered but not evaluated.
</p>
<sample src="examples/SSTIGood.rb" />
</example>
<references>
<li>
Wikipedia: <a href="https://en.wikipedia.org/wiki/Code_injection#Server_Side_Template_Injection">Server Side Template Injection</a>.
</li>
<li>
Portswigger : <a href="https://portswigger.net/web-security/server-side-template-injection">Server Side Template Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name Server-side template injection
* @description Building a server-side template from user-controlled sources is vulnerable to
* insertion of malicious code by the user.
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id rb/server-side-template-injection
* @tags security
* external/cwe/cwe-94
*/
import codeql.ruby.DataFlow
import codeql.ruby.security.TemplateInjectionQuery
import DataFlow::PathGraph
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This template depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,24 @@
require 'erb'
require 'slim'
class BadERBController < ActionController::Base
def some_request_handler
name = params["name"]
html_text = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
template = ERB.new(html_text).result(binding)
end
end
class BadSlimController < ActionController::Base
def some_request_handler
name = params["name"]
html_text = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
Slim::Template.new{ html_text }.render
end
end

View File

@@ -0,0 +1,26 @@
require 'erb'
require 'slim'
class GoodController < ActionController::Base
def some_request_handler
name = params["name"]
html_text = "
<!DOCTYPE html><html><body>
<h2>Hello <%= name %> </h2></body></html>
"
template = ERB.new(html_text).result(binding)
end
end
class GoodController < ActionController::Base
def some_request_handler
name = params["name"]
html_text = "
<!DOCTYPE html>
html
body
h2 == name;
"
Slim::Template.new{ html_text }.render(Object.new, name: name)
end
end

View File

@@ -2805,6 +2805,7 @@
| file://:0:0:0:0 | parameter position 0 of ActionController::Parameters#merge! | file://:0:0:0:0 | [summary] to write: return (return) in ActionController::Parameters#merge! |
| file://:0:0:0:0 | parameter position 0 of Arel.sql | file://:0:0:0:0 | [summary] to write: return (return) in Arel.sql |
| file://:0:0:0:0 | parameter position 0 of Base64.decode64() | file://:0:0:0:0 | [summary] to write: return (return) in Base64.decode64() |
| file://:0:0:0:0 | parameter position 0 of ERB.new | file://:0:0:0:0 | [summary] to write: return (return) in ERB.new |
| file://:0:0:0:0 | parameter position 0 of File.absolute_path | file://:0:0:0:0 | [summary] to write: return (return) in File.absolute_path |
| file://:0:0:0:0 | parameter position 0 of File.dirname | file://:0:0:0:0 | [summary] to write: return (return) in File.dirname |
| file://:0:0:0:0 | parameter position 0 of File.expand_path | file://:0:0:0:0 | [summary] to write: return (return) in File.expand_path |

View File

@@ -0,0 +1,59 @@
class FooController < ActionController::Base
def some_request_handler
# A string tainted by user input is inserted into a template
# (i.e a remote flow source)
name = params[:name]
# Template with the source
bad_text = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
# BAD: user input is evaluated
# where name is unsanitized
template = ERB.new(bad_text).result(binding)
# Template with the source
good_text = "
<!DOCTYPE html><html><body>
<h2>Hello <%= name %> </h2></body></html>
"
# GOOD: user input is not evaluated
template2 = ERB.new(good_text).result(binding)
end
end
class BarController < ApplicationController
def safe_paths
name1 = params["name1"]
# GOOD: barrier guard prevents taint flow
if name == "admin"
text_bar1 = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
else
text = "
<!DOCTYPE html><html><body>
<h2>Hello else </h2></body></html>
"
end
template_bar1 = ERB.new(text_bar1).result(binding)
name2 = params["name2"]
# GOOD: barrier guard prevents taint flow
name2 = if ["admin", "guest"].include? name2
name2
else
name2 = "none"
end
text_bar2 = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name2
template_bar2 = ERB.new(text_bar2).result(binding)
end
end

View File

@@ -0,0 +1,67 @@
class FooController < ActionController::Base
def some_request_handler
# A string tainted by user input is inserted into a template
# (i.e a remote flow source)
name = params[:name]
# Template with the source (no sanitizer)
bad_text = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
# BAD: renders user input
# where text is unsanitized
Slim::Template.new{ bad_text }.render
# Template with the source (no sanitizer)
bad2_text = "
<!DOCTYPE html><html><body>
<h2>Hello #{name} </h2></body></html>
"
# BAD: renders user input
# where text is unsanitized
Slim::Template.new{ bad2_text }.render
# Template with the source (no render)
good_text = "
<!DOCTYPE html>
html
body
h2 == name;
"
# GOOD: user input is not evaluated
Slim::Template.new{ good_text }.render(Object.new, name: name)
end
end
class BarController < ApplicationController
def safe_paths
name1 = params["name1"]
# GOOD: barrier guard prevents taint flow
if name == "admin"
text_bar1 = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name
else
text_bar1 = "
<!DOCTYPE html><html><body>
<h2>Hello else </h2></body></html>
"
end
template_bar1 = Slim::Template.new{ text_bar1 }.render
name2 = params["name2"]
# GOOD: barrier guard prevents taint flow
name2 = if ["admin", "guest"].include? name2
name2
else
name2 = "none"
end
text_bar2 = "
<!DOCTYPE html><html><body>
<h2>Hello %s </h2></body></html>
" % name2
template_bar1 = Slim::Template.new{ text_bar2 }.render
end
end

View File

@@ -0,0 +1,33 @@
edges
| ErbInjection.rb:5:12:5:17 | call to params : | ErbInjection.rb:5:12:5:24 | ...[...] : |
| ErbInjection.rb:5:12:5:24 | ...[...] : | ErbInjection.rb:11:11:11:14 | name : |
| ErbInjection.rb:5:12:5:24 | ...[...] : | ErbInjection.rb:15:24:15:31 | bad_text |
| ErbInjection.rb:8:16:11:14 | ... % ... : | ErbInjection.rb:15:24:15:31 | bad_text |
| ErbInjection.rb:11:11:11:14 | name : | ErbInjection.rb:8:16:11:14 | ... % ... : |
| SlimInjection.rb:5:12:5:17 | call to params : | SlimInjection.rb:5:12:5:24 | ...[...] : |
| SlimInjection.rb:5:12:5:24 | ...[...] : | SlimInjection.rb:8:5:11:14 | ... = ... : |
| SlimInjection.rb:5:12:5:24 | ...[...] : | SlimInjection.rb:11:11:11:14 | name : |
| SlimInjection.rb:5:12:5:24 | ...[...] : | SlimInjection.rb:17:5:20:7 | ... = ... : |
| SlimInjection.rb:8:5:11:14 | ... = ... : | SlimInjection.rb:14:25:14:32 | bad_text |
| SlimInjection.rb:8:16:11:14 | ... % ... : | SlimInjection.rb:8:5:11:14 | ... = ... : |
| SlimInjection.rb:11:11:11:14 | name : | SlimInjection.rb:8:16:11:14 | ... % ... : |
| SlimInjection.rb:17:5:20:7 | ... = ... : | SlimInjection.rb:23:25:23:33 | bad2_text |
nodes
| ErbInjection.rb:5:12:5:17 | call to params : | semmle.label | call to params : |
| ErbInjection.rb:5:12:5:24 | ...[...] : | semmle.label | ...[...] : |
| ErbInjection.rb:8:16:11:14 | ... % ... : | semmle.label | ... % ... : |
| ErbInjection.rb:11:11:11:14 | name : | semmle.label | name : |
| ErbInjection.rb:15:24:15:31 | bad_text | semmle.label | bad_text |
| SlimInjection.rb:5:12:5:17 | call to params : | semmle.label | call to params : |
| SlimInjection.rb:5:12:5:24 | ...[...] : | semmle.label | ...[...] : |
| SlimInjection.rb:8:5:11:14 | ... = ... : | semmle.label | ... = ... : |
| SlimInjection.rb:8:16:11:14 | ... % ... : | semmle.label | ... % ... : |
| SlimInjection.rb:11:11:11:14 | name : | semmle.label | name : |
| SlimInjection.rb:14:25:14:32 | bad_text | semmle.label | bad_text |
| SlimInjection.rb:17:5:20:7 | ... = ... : | semmle.label | ... = ... : |
| SlimInjection.rb:23:25:23:33 | bad2_text | semmle.label | bad2_text |
subpaths
#select
| ErbInjection.rb:15:24:15:31 | bad_text | ErbInjection.rb:5:12:5:17 | call to params : | ErbInjection.rb:15:24:15:31 | bad_text | This template depends on a $@. | ErbInjection.rb:5:12:5:17 | call to params | user-provided value |
| SlimInjection.rb:14:25:14:32 | bad_text | SlimInjection.rb:5:12:5:17 | call to params : | SlimInjection.rb:14:25:14:32 | bad_text | This template depends on a $@. | SlimInjection.rb:5:12:5:17 | call to params | user-provided value |
| SlimInjection.rb:23:25:23:33 | bad2_text | SlimInjection.rb:5:12:5:17 | call to params : | SlimInjection.rb:23:25:23:33 | bad2_text | This template depends on a $@. | SlimInjection.rb:5:12:5:17 | call to params | user-provided value |

View File

@@ -0,0 +1 @@
experimental/template-injection/TemplateInjection.ql