Skip to content

Commit e65d722

Browse files
committed
Ruby: tests, patterns, fix erb flow
1 parent eada3b9 commit e65d722

File tree

7 files changed

+268
-12
lines changed

7 files changed

+268
-12
lines changed

ruby/ql/lib/codeql/ruby/frameworks/Sinatra.qll

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ private import codeql.ruby.dataflow.FlowSummary
88

99
/** Provides modeling for the Sinatra library. */
1010
module Sinatra {
11-
private class App extends DataFlow::ClassNode {
11+
class App extends DataFlow::ClassNode {
1212
App() { this = DataFlow::getConstant("Sinatra").getConstant("Base").getADescendentModule() }
1313

14-
Route getRoute() { result.getApp() = this }
14+
Route getARoute() { result.getApp() = this }
1515
}
1616

17-
private class Route extends DataFlow::CallNode {
17+
class Route extends DataFlow::CallNode {
1818
private App app;
1919

2020
Route() {
@@ -44,22 +44,26 @@ module Sinatra {
4444
}
4545
}
4646

47-
private class ErbCall extends DataFlow::CallNode {
47+
class ErbCall extends DataFlow::CallNode {
4848
private Route route;
4949

5050
ErbCall() {
5151
this.asExpr().getExpr().getEnclosingCallable() = route.getBody().asCallableAstNode() and
5252
this.getMethodName() = "erb"
5353
}
5454

55-
ErbFile getTemplateFile() {
56-
result.getTemplateName() =
57-
this.getArgument(0).asExpr().getConstantValue().getStringlikeValue() and
58-
result.getRelativePath().matches("%views/%")
59-
}
55+
/**
56+
* Gets the template file corresponding to this call.
57+
*/
58+
ErbFile getTemplateFile() { result = getTemplateFile(this.asExpr().getExpr()) }
6059
}
6160

62-
ErbFile getTemplateFile(MethodCall erbCall) {
61+
/**
62+
* Gets the template file referred to by `erbCall`.
63+
* This works on the AST level to avoid non-monotonic reecursion in `ErbLocalsHashSyntheticGlobal`.
64+
*/
65+
private ErbFile getTemplateFile(MethodCall erbCall) {
66+
erbCall.getMethodName() = "erb" and
6367
result.getTemplateName() = erbCall.getArgument(0).getConstantValue().getStringlikeValue() and
6468
result.getRelativePath().matches("%views/%")
6569
}
@@ -73,7 +77,7 @@ module Sinatra {
7377
loc.getEndLine() + ":" + loc.getEndColumn()
7478
}
7579

76-
private class ErbLocalsHashSyntheticGlobal extends SummaryComponent::SyntheticGlobal {
80+
class ErbLocalsHashSyntheticGlobal extends SummaryComponent::SyntheticGlobal {
7781
private string id;
7882
private MethodCall erbCall;
7983
private ErbFile erbFile;
@@ -98,7 +102,9 @@ module Sinatra {
98102
override MethodCall getACall() { result = any(ErbCall c).asExpr().getExpr() }
99103

100104
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
101-
input = "Argument[2]" and output = "SyntheticGlobal[" + global + "]" and preservesValue = true
105+
input = "Argument[locals:]" and
106+
output = "SyntheticGlobal[" + global + "]" and
107+
preservesValue = true
102108
}
103109
}
104110

@@ -124,4 +130,43 @@ module Sinatra {
124130
preservesValue = true
125131
}
126132
}
133+
134+
/**
135+
* Filters are run before or after the route handler. They can modify the
136+
* request and response, and share instance variables with the route handler.
137+
*/
138+
class Filter extends DataFlow::CallNode {
139+
private App app;
140+
141+
Filter() { this = app.getAModuleLevelCall(["before", "after"]) }
142+
143+
/**
144+
* Gets the pattern which constrains this route, if any. In the example below, the pattern is `/protected/*`.
145+
* Patterns are typically given as strings, and are interpreted by the `mustermann` gem (they are not regular expressions).
146+
* ```rb
147+
* before '/protected/*' do
148+
* authenticate!
149+
* end
150+
* ```
151+
*/
152+
DataFlow::ExprNode getPattern() { result = this.getArgument(0) }
153+
154+
/**
155+
* Holds if this filter has a pattern.
156+
*/
157+
predicate hasPattern() { exists(this.getPattern()) }
158+
159+
/**
160+
* Gets the body of this filter.
161+
*/
162+
DataFlow::BlockNode getBody() { result = this.getBlock() }
163+
}
164+
165+
class BeforeFilter extends Filter {
166+
BeforeFilter() { this.getMethodName() = "before" }
167+
}
168+
169+
class AfterFilter extends Filter {
170+
AfterFilter() { this.getMethodName() = "after" }
171+
}
127172
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
failures
2+
| views/index.erb:2:10:2:12 | call to foo | Unexpected result: hasTaintFlow= |
3+
edges
4+
| app.rb:75:5:75:8 | [post] self [@foo] : | app.rb:76:32:76:35 | self [@foo] : |
5+
| app.rb:75:12:75:17 | call to params : | app.rb:75:12:75:24 | ...[...] : |
6+
| app.rb:75:12:75:24 | ...[...] : | app.rb:75:5:75:8 | [post] self [@foo] : |
7+
| app.rb:76:32:76:35 | @foo : | views/index.erb:2:10:2:12 | call to foo |
8+
| app.rb:76:32:76:35 | self [@foo] : | app.rb:76:32:76:35 | @foo : |
9+
nodes
10+
| app.rb:75:5:75:8 | [post] self [@foo] : | semmle.label | [post] self [@foo] : |
11+
| app.rb:75:12:75:17 | call to params : | semmle.label | call to params : |
12+
| app.rb:75:12:75:24 | ...[...] : | semmle.label | ...[...] : |
13+
| app.rb:76:32:76:35 | @foo : | semmle.label | @foo : |
14+
| app.rb:76:32:76:35 | self [@foo] : | semmle.label | self [@foo] : |
15+
| views/index.erb:2:10:2:12 | call to foo | semmle.label | call to foo |
16+
subpaths
17+
#select
18+
| views/index.erb:2:10:2:12 | call to foo | app.rb:75:12:75:17 | call to params : | views/index.erb:2:10:2:12 | call to foo | $@ | app.rb:75:12:75:17 | call to params : | call to params : |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @kind path-problem
3+
*/
4+
5+
import ruby
6+
import TestUtilities.InlineFlowTest
7+
import PathGraph
8+
import codeql.ruby.frameworks.Sinatra
9+
import codeql.ruby.Concepts
10+
11+
class ParamsTaintFlowConf extends DefaultTaintFlowConf {
12+
override predicate isSource(DataFlow::Node n) { n instanceof Http::Server::RequestInputAccess }
13+
}
14+
15+
from DataFlow::PathNode source, DataFlow::PathNode sink, ParamsTaintFlowConf conf
16+
where conf.hasFlowPath(source, sink)
17+
select sink, source, sink, "$@", source, source.toString()
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
routes
2+
| app.rb:1:1:105:3 | MyApp | app.rb:2:3:4:5 | call to get |
3+
| app.rb:1:1:105:3 | MyApp | app.rb:6:3:8:5 | call to get |
4+
| app.rb:1:1:105:3 | MyApp | app.rb:10:3:13:5 | call to get |
5+
| app.rb:1:1:105:3 | MyApp | app.rb:15:3:18:5 | call to get |
6+
| app.rb:1:1:105:3 | MyApp | app.rb:20:3:22:5 | call to get |
7+
| app.rb:1:1:105:3 | MyApp | app.rb:24:3:26:5 | call to get |
8+
| app.rb:1:1:105:3 | MyApp | app.rb:28:3:31:5 | call to get |
9+
| app.rb:1:1:105:3 | MyApp | app.rb:33:3:35:5 | call to get |
10+
| app.rb:1:1:105:3 | MyApp | app.rb:37:3:42:5 | call to get |
11+
| app.rb:1:1:105:3 | MyApp | app.rb:44:3:46:5 | call to get |
12+
| app.rb:1:1:105:3 | MyApp | app.rb:48:3:50:5 | call to get |
13+
| app.rb:1:1:105:3 | MyApp | app.rb:52:3:54:5 | call to get |
14+
| app.rb:1:1:105:3 | MyApp | app.rb:56:3:58:5 | call to get |
15+
| app.rb:1:1:105:3 | MyApp | app.rb:60:3:62:5 | call to get |
16+
| app.rb:1:1:105:3 | MyApp | app.rb:66:3:68:5 | call to get |
17+
| app.rb:1:1:105:3 | MyApp | app.rb:70:3:72:5 | call to get |
18+
| app.rb:1:1:105:3 | MyApp | app.rb:74:3:77:5 | call to get |
19+
| app.rb:1:1:105:3 | MyApp | app.rb:79:3:82:5 | call to get |
20+
| app.rb:1:1:105:3 | MyApp | app.rb:89:3:92:5 | call to get |
21+
params
22+
| app.rb:3:14:3:19 | call to params |
23+
| app.rb:12:5:12:10 | call to params |
24+
| app.rb:17:5:17:10 | call to params |
25+
| app.rb:25:15:25:20 | call to params |
26+
| app.rb:39:13:39:18 | call to params |
27+
| app.rb:40:14:40:19 | call to params |
28+
| app.rb:45:38:45:43 | call to params |
29+
| app.rb:75:12:75:17 | call to params |
30+
| app.rb:91:5:91:10 | call to params |
31+
erbCalls
32+
| app.rb:76:5:76:36 | call to erb | views/index.erb:0:0:0:0 | views/index.erb |
33+
erbSyntheticGlobals
34+
| SinatraErbLocalsHash(library-tests/frameworks/sinatra/views/index.erb,library-tests/frameworks/sinatra/app.rb@76:5:76:36) | views/index.erb:0:0:0:0 | views/index.erb |
35+
filters
36+
| app.rb:84:3:87:5 | call to before | before |
37+
| app.rb:94:3:96:5 | call to after | after |
38+
| app.rb:98:3:100:5 | call to before | before |
39+
| app.rb:102:3:104:5 | call to after | after |
40+
filterPatterns
41+
| app.rb:98:3:100:5 | call to before | app.rb:98:10:98:23 | "/protected/*" |
42+
| app.rb:102:3:104:5 | call to after | app.rb:102:9:102:23 | "/create/:slug" |
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ruby
2+
import codeql.ruby.frameworks.Sinatra
3+
import codeql.ruby.Concepts
4+
import codeql.ruby.AST
5+
6+
query predicate routes(Sinatra::App app, Sinatra::Route route) { route = app.getARoute() }
7+
8+
query predicate params(Http::Server::RequestInputAccess params) { any() }
9+
10+
query predicate erbCalls(Sinatra::ErbCall c, ErbFile templateFile) {
11+
templateFile = c.getTemplateFile()
12+
}
13+
14+
query predicate erbSyntheticGlobals(Sinatra::ErbLocalsHashSyntheticGlobal g, ErbFile file) {
15+
file = g.getErbFile()
16+
}
17+
18+
query predicate filters(Sinatra::Filter filter, string kind) {
19+
filter instanceof Sinatra::BeforeFilter and kind = "before"
20+
or
21+
filter instanceof Sinatra::AfterFilter and kind = "after"
22+
}
23+
24+
query predicate filterPatterns(Sinatra::Filter filter, DataFlow::ExprNode pattern) {
25+
pattern = filter.getPattern()
26+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
class MyApp < Sinatra::Base
2+
get '/hello/:name' do
3+
"Hello #{params['name']}!"
4+
end
5+
6+
get '/goodbye/:name' do |n|
7+
"Goodbyte #{n}!"
8+
end
9+
10+
get '/say/*/to/*' do
11+
# matches /say/hello/to/world
12+
params['splat'] # => ["hello", "world"]
13+
end
14+
15+
get '/download/*.*' do
16+
# matches /download/path/to/file.xml
17+
params['splat'] # => ["path/to/file", "xml"]
18+
end
19+
20+
get '/download/*.*' do |path, ext|
21+
[path, ext] # => ["path/to/file", "xml"]
22+
end
23+
24+
get /\/hello\/([\w]+)/ do
25+
"Hello, #{params['captures'].first}!"
26+
end
27+
28+
get %r{/hello/([\w]+)} do |c|
29+
# Matches "GET /meta/hello/world", "GET /hello/world/1234" etc.
30+
"Hello, #{c}!"
31+
end
32+
33+
get '/posts/:format?' do
34+
# matches "GET /posts/" and any extension "GET /posts/json", "GET /posts/xml" etc
35+
end
36+
37+
get '/posts' do
38+
# matches "GET /posts?title=foo&author=bar"
39+
title = params['title']
40+
author = params['author']
41+
# uses title and author variables; query is optional to the /posts route
42+
end
43+
44+
get '/foo', :agent => /Songbird (\d\.\d)[\d\/]*?/ do
45+
"You're using Songbird version #{params['agent'][0]}"
46+
end
47+
48+
get '/foo' do
49+
# Matches non-songbird browsers
50+
end
51+
52+
get '/', :host_name => /^admin\./ do
53+
"Admin Area, Access denied!"
54+
end
55+
56+
get '/', :provides => 'html' do
57+
haml :index
58+
end
59+
60+
get '/', :provides => ['rss', 'atom', 'xml'] do
61+
builder :feed
62+
end
63+
64+
set(:probability) { |value| condition { rand <= value } }
65+
66+
get '/win_a_car', :probability => 0.1 do
67+
"You won!"
68+
end
69+
70+
get '/win_a_car' do
71+
"Sorry, you lost."
72+
end
73+
74+
get '/' do
75+
@foo = params["foo"]
76+
erb :index, locals: {foo: @foo}
77+
end
78+
79+
get '/' do
80+
code = "<%= Time.now %>"
81+
erb code
82+
end
83+
84+
before do
85+
@note = 'Hi!'
86+
request.path_info = '/foo/bar/baz'
87+
end
88+
89+
get '/foo/*' do
90+
@note #=> 'Hi!'
91+
params['splat'] #=> 'bar/baz'
92+
end
93+
94+
after do
95+
puts response.status
96+
end
97+
98+
before '/protected/*' do
99+
authenticate!
100+
end
101+
102+
after '/create/:slug' do |slug|
103+
session[:last_slug] = slug
104+
end
105+
end
106+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<%= @foo %>
2+
<%= sink foo %>

0 commit comments

Comments
 (0)