Skip to content

Commit ff50513

Browse files
committed
Add initial query for Ruby SSTI
1 parent 89aec09 commit ff50513

File tree

15 files changed

+492
-0
lines changed

15 files changed

+492
-0
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,69 @@ module Cryptography {
10591059

10601060
class BlockMode = SC::BlockMode;
10611061
}
1062+
1063+
/**
1064+
* A data-flow node that constructs a template.
1065+
*
1066+
* Often, it is worthy of an alert if a template is constructed such that
1067+
* executing it would be a security risk.
1068+
*
1069+
* If it is important that the template is rendered, use `TemplateRendering`.
1070+
*
1071+
* Extend this class to refine existing API models. If you want to model new APIs,
1072+
* extend `TemplateConstruction::Range` instead.
1073+
*/
1074+
class TemplateConstruction extends DataFlow::Node instanceof TemplateConstruction::Range {
1075+
/** Gets the argument that specifies the template to be constructed. */
1076+
DataFlow::Node getTemplate() { result = super.getTemplate() }
1077+
}
1078+
1079+
/** Provides a class for modeling new template rendering APIs. */
1080+
module TemplateConstruction {
1081+
/**
1082+
* A data-flow node that constructs a template.
1083+
*
1084+
* Often, it is worthy of an alert if a template is constructed such that
1085+
* executing it would be a security risk.
1086+
*
1087+
* If it is important that the template is rendered, use `TemplateRendering`.
1088+
*
1089+
* Extend this class to model new APIs. If you want to refine existing API models,
1090+
* extend `TemplateConstruction` instead.
1091+
*/
1092+
abstract class Range extends DataFlow::Node {
1093+
/** Gets the argument that specifies the template to be constructed. */
1094+
abstract DataFlow::Node getTemplate();
1095+
}
1096+
}
1097+
1098+
/**
1099+
* A data-flow node that renders templates.
1100+
*
1101+
* If the context of interest is such that merely constructing a template
1102+
* would be valuable to report, consider using `TemplateConstruction`.
1103+
*
1104+
* Extend this class to refine existing API models. If you want to model new APIs,
1105+
* extend `TemplateRendering::Range` instead.
1106+
*/
1107+
class TemplateRendering extends DataFlow::Node instanceof TemplateRendering::Range {
1108+
/** Gets the argument that specifies the template to be rendered. */
1109+
DataFlow::Node getTemplate() { result = super.getTemplate() }
1110+
}
1111+
1112+
/** Provides a class for modeling new template rendering APIs. */
1113+
module TemplateRendering {
1114+
/**
1115+
* A data-flow node that renders templates.
1116+
*
1117+
* If the context of interest is such that merely constructing a template
1118+
* would be valuable to report, consider using `TemplateConstruction`.
1119+
*
1120+
* Extend this class to model new APIs. If you want to refine existing API models,
1121+
* extend `TemplateRendering` instead.
1122+
*/
1123+
abstract class Range extends DataFlow::Node {
1124+
/** Gets the argument that specifies the template to be rendered. */
1125+
abstract DataFlow::Node getTemplate();
1126+
}
1127+
}

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ private import codeql.ruby.frameworks.ActionDispatch
2727
private import codeql.ruby.frameworks.PosixSpawn
2828
private import codeql.ruby.frameworks.StringFormatters
2929
private import codeql.ruby.frameworks.Json
30+
private import codeql.ruby.frameworks.Erb
31+
private import codeql.ruby.frameworks.Slim
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
3+
*/
4+
5+
private import codeql.ruby.ApiGraphs
6+
private import codeql.ruby.dataflow.FlowSummary
7+
private import codeql.ruby.Concepts
8+
9+
/**
10+
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
11+
*/
12+
module ERB {
13+
/**
14+
* Flow summary for `ERB.new`. This method wraps a template string, compiling it.
15+
*/
16+
private class TemplateSummary extends SummarizedCallable {
17+
TemplateSummary() { this = "ERB.new" }
18+
19+
override MethodCall getACall() {
20+
result = API::getTopLevelMember("ERB").getAMethodCall("new").asExpr().getExpr()
21+
}
22+
23+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
24+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
25+
}
26+
}
27+
28+
/** A call to `ERB.new`, considered as a template construction. */
29+
private class ERBTemplateNewCall extends TemplateConstruction::Range, DataFlow::CallNode {
30+
ERBTemplateNewCall() { this = API::getTopLevelMember("ERB").getAMethodCall("new") }
31+
32+
override DataFlow::Node getTemplate() { result = this.getArgument(0) }
33+
}
34+
35+
/** A call to `ERB.new(foo).result(binding)`, considered as a template rendering. */
36+
private class ERBTemplateRendering extends TemplateRendering::Range, DataFlow::CallNode {
37+
DataFlow::Node template;
38+
39+
ERBTemplateRendering() {
40+
exists(ERBTemplateNewCall templateConstruction |
41+
this = templateConstruction.getAMethodCall("result") and
42+
template = templateConstruction.getTemplate()
43+
)
44+
}
45+
46+
override DataFlow::Node getTemplate() { result = template }
47+
}
48+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
3+
*/
4+
5+
private import codeql.ruby.ApiGraphs
6+
private import codeql.ruby.dataflow.FlowSummary
7+
private import codeql.ruby.Concepts
8+
9+
/**
10+
* Provides templating for embedding Ruby code into text files, allowing dynamic content generation in web applications.
11+
*/
12+
module Slim {
13+
/** A call to `Slim::Template.new`, considered as a template construction. */
14+
private class SlimTemplateNewCall extends TemplateConstruction::Range, DataFlow::CallNode {
15+
SlimTemplateNewCall() {
16+
this = API::getTopLevelMember("Slim").getMember("Template").getAMethodCall("new")
17+
}
18+
19+
override DataFlow::Node getTemplate() {
20+
result.asExpr().getExpr() =
21+
this.getBlock().(DataFlow::BlockNode).asCallableAstNode().getAStmt()
22+
}
23+
}
24+
25+
/** A call to `Slim::Template.new{ foo }.render`, considered as a template rendering */
26+
private class SlimTemplateRendering extends TemplateRendering::Range, DataFlow::CallNode {
27+
DataFlow::Node template;
28+
29+
SlimTemplateRendering() {
30+
exists(SlimTemplateNewCall templateConstruction |
31+
this = templateConstruction.getAMethodCall("render") and
32+
template = templateConstruction.getTemplate()
33+
)
34+
}
35+
36+
override DataFlow::Node getTemplate() { result = template }
37+
}
38+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* ERB Server Side Template Injections, as well as extension points for adding your own
4+
*/
5+
6+
private import codeql.ruby.Concepts
7+
private import codeql.ruby.DataFlow
8+
private import codeql.ruby.dataflow.BarrierGuards
9+
private import codeql.ruby.dataflow.RemoteFlowSources
10+
import codeql.ruby.ApiGraphs
11+
import codeql.ruby.AST
12+
13+
/**
14+
* Provides default sources, sinks and sanitizers for detecting
15+
* Server Side Template Injections, as well as extension points for adding your own
16+
*/
17+
module TemplateInjection {
18+
/** A data flow source for SSTI vulnerabilities */
19+
abstract class Source extends DataFlow::Node { }
20+
21+
/** A data flow sink for SSTI vulnerabilities */
22+
abstract class Sink extends DataFlow::Node { }
23+
24+
/** A sanitizer for SSTI vulnerabilities. */
25+
abstract class Sanitizer extends DataFlow::Node { }
26+
27+
/**
28+
* A source of remote user input, considered as a flow source.
29+
*/
30+
private class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
31+
32+
/**
33+
* An Server Side Template Injection rendering, considered as a flow sink.
34+
*/
35+
private class TemplateRenderingAsSink extends Sink {
36+
TemplateRenderingAsSink() { this = any(TemplateRendering e).getTemplate() }
37+
}
38+
39+
/**
40+
* A comparison with a constant string, considered as a sanitizer-guard.
41+
*/
42+
private class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
43+
44+
/**
45+
* An inclusion check against an array of constant strings, considered as a
46+
* sanitizer-guard.
47+
*/
48+
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
49+
StringConstArrayInclusionCallBarrier { }
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* Server Side Template Injections, as well as extension points for adding your own
4+
*/
5+
6+
private import codeql.ruby.DataFlow
7+
private import codeql.ruby.TaintTracking
8+
import TemplateInjectionCustomizations::TemplateInjection
9+
10+
/**
11+
* A taint-tracking configuration for detecting Server Side Template Injections vulnerabilities.
12+
*/
13+
class Configuration extends TaintTracking::Configuration {
14+
Configuration() { this = "TemplateInjection" }
15+
16+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
17+
18+
override predicate isSink(DataFlow::Node source) { source instanceof Sink }
19+
20+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
21+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Template Injection occurs when user input is embedded in a template's code in an unsafe manner.
6+
An attacker can use native template syntax to inject a malicious payload into a template, which is then executed server-side.
7+
This permits the attacker to run arbitrary code in the server's context.
8+
</p>
9+
</overview>
10+
11+
<recommendation>
12+
<p>
13+
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,
14+
use a sandboxed environment where access to unsafe attributes and methods is prohibited.
15+
</p>
16+
</recommendation>
17+
18+
<example>
19+
<p>
20+
<p>Consider the example given below, an untrusted HTTP parameter `name` is used to generate a template string. This can lead to remote code execution. </p>
21+
<sample src="examples/SSTIBad.rb" />
22+
23+
<p>Here we have fixed the problem by including ERB/Slim syntax in the string, then the user input will be rendered but no evaluated.</p>
24+
<sample src="examples/SSTIGood.rb" />
25+
</example>
26+
27+
<references>
28+
<li>
29+
Wikipedia: <a href="https://en.wikipedia.org/wiki/Code_injection#Server_Side_Template_Injection">Server Side Template Injection</a>.
30+
</li>
31+
<li>
32+
Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)
33+
</li>
34+
</references>
35+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name Server-side template injection
3+
* @description Building a server-side template from user-controlled sources is vulnerable to
4+
* insertion of malicious code by the user.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 8.8
8+
* @precision high
9+
* @id rb/ssti
10+
* @tags security
11+
* external/cwe/cwe-94
12+
*/
13+
14+
import codeql.ruby.DataFlow
15+
import codeql.ruby.security.TemplateInjectionQuery
16+
import DataFlow::PathGraph
17+
18+
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where config.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "This template depends on a $@.", source.getNode(),
21+
"user-provided value"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'erb'
2+
require 'slim'
3+
4+
class BadERBController < ActionController::Base
5+
def some_request_handler
6+
name = params["name"]
7+
html_text = "
8+
<!DOCTYPE html><html><body>
9+
<h2>Hello %s </h2></body></html>
10+
" % name
11+
template = ERB.new(html_text).result(binding)
12+
end
13+
end
14+
15+
class BadSlimController < ActionController::Base
16+
def some_request_handler
17+
name = params["name"]
18+
html_text = "
19+
<!DOCTYPE html><html><body>
20+
<h2>Hello %s </h2></body></html>
21+
" % name
22+
Slim::Template.new{ html_text }.render
23+
end
24+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'erb'
2+
require 'slim'
3+
4+
class GoodController < ActionController::Base
5+
def some_request_handler
6+
name = params["name"]
7+
html_text = "
8+
<!DOCTYPE html><html><body>
9+
<h2>Hello <%= name %> </h2></body></html>
10+
"
11+
template = ERB.new(html_text).result(binding)
12+
end
13+
end
14+
15+
class GoodController < ActionController::Base
16+
def some_request_handler
17+
name = params["name"]
18+
html_text = "
19+
<!DOCTYPE html>
20+
html
21+
body
22+
h2 == name;
23+
"
24+
Slim::Template.new{ html_text }.render(Object.new, name: name)
25+
end
26+
end

0 commit comments

Comments
 (0)