Skip to content

Commit d295acc

Browse files
committed
Add initial support for Ruby Grape
1 parent e8ddac0 commit d295acc

File tree

7 files changed

+316
-0
lines changed

7 files changed

+316
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ private import codeql.ruby.frameworks.Rails
2121
private import codeql.ruby.frameworks.Railties
2222
private import codeql.ruby.frameworks.Stdlib
2323
private import codeql.ruby.frameworks.Files
24+
private import codeql.ruby.frameworks.Grape
2425
private import codeql.ruby.frameworks.HttpClients
2526
private import codeql.ruby.frameworks.XmlParsing
2627
private import codeql.ruby.frameworks.ActionDispatch
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* Provides modeling for the `Grape` API framework.
3+
*/
4+
5+
private import codeql.ruby.AST
6+
private import codeql.ruby.Concepts
7+
private import codeql.ruby.controlflow.CfgNodes
8+
private import codeql.ruby.DataFlow
9+
private import codeql.ruby.dataflow.RemoteFlowSources
10+
private import codeql.ruby.ApiGraphs
11+
private import codeql.ruby.typetracking.TypeTracking
12+
private import codeql.ruby.frameworks.Rails
13+
private import codeql.ruby.frameworks.internal.Rails
14+
private import codeql.ruby.dataflow.internal.DataFlowDispatch
15+
16+
/**
17+
* Provides modeling for Grape, a REST-like API framework for Ruby.
18+
* Grape allows you to build RESTful APIs in Ruby with minimal effort.
19+
*/
20+
module Grape {
21+
/**
22+
* A Grape API class which sits at the top of the class hierarchy.
23+
* In other words, it does not subclass any other Grape API class in source code.
24+
*/
25+
class RootAPI extends GrapeAPIClass {
26+
RootAPI() {
27+
not exists(GrapeAPIClass parent | this != parent and this = parent.getADescendent())
28+
}
29+
}
30+
}
31+
32+
/**
33+
* A class that extends `Grape::API`.
34+
* For example,
35+
*
36+
* ```rb
37+
* class FooAPI < Grape::API
38+
* get '/users' do
39+
* name = params[:name]
40+
* User.where("name = #{name}")
41+
* end
42+
* end
43+
* ```
44+
*/
45+
class GrapeAPIClass extends DataFlow::ClassNode {
46+
GrapeAPIClass() {
47+
this = grapeAPIBaseClass().getADescendentModule() and
48+
not exists(DataFlow::ModuleNode m | m = grapeAPIBaseClass().asModule() | this = m)
49+
}
50+
51+
/**
52+
* Gets a `GrapeEndpoint` defined in this class.
53+
*/
54+
GrapeEndpoint getAnEndpoint() {
55+
result.getAPIClass() = this
56+
}
57+
58+
/**
59+
* Gets a `self` that possibly refers to an instance of this class.
60+
*/
61+
DataFlow::LocalSourceNode getSelf() {
62+
result = this.getAnInstanceSelf()
63+
or
64+
// Include the module-level `self` to recover some cases where a block at the module level
65+
// is invoked with an instance as the `self`.
66+
result = this.getModuleLevelSelf()
67+
}
68+
}
69+
70+
private DataFlow::ConstRef grapeAPIBaseClass() {
71+
result = DataFlow::getConstant("Grape").getConstant("API")
72+
}
73+
74+
private API::Node grapeAPIInstance() {
75+
result = any(GrapeAPIClass cls).getSelf().track()
76+
}
77+
78+
/**
79+
* A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
80+
*/
81+
class GrapeEndpoint extends DataFlow::CallNode {
82+
private GrapeAPIClass apiClass;
83+
84+
GrapeEndpoint() {
85+
this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
86+
}
87+
88+
/**
89+
* Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
90+
*/
91+
string getHttpMethod() {
92+
result = this.getMethodName().toUpperCase()
93+
}
94+
95+
/**
96+
* Gets the API class containing this endpoint.
97+
*/
98+
GrapeAPIClass getAPIClass() { result = apiClass }
99+
100+
/**
101+
* Gets the block containing the endpoint logic.
102+
*/
103+
DataFlow::BlockNode getBody() { result = this.getBlock() }
104+
105+
/**
106+
* Gets the path pattern for this endpoint, if specified.
107+
*/
108+
string getPath() {
109+
result = this.getArgument(0).getConstantValue().getString()
110+
}
111+
}
112+
113+
/**
114+
* A `RemoteFlowSource::Range` to represent accessing the
115+
* Grape parameters available via the `params` method within an endpoint.
116+
*/
117+
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
118+
GrapeParamsSource() {
119+
this.asExpr().getExpr() instanceof GrapeParamsCall
120+
}
121+
122+
override string getSourceType() { result = "Grape::API#params" }
123+
124+
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
125+
}
126+
127+
/**
128+
* A call to `params` from within a Grape API endpoint.
129+
*/
130+
private class GrapeParamsCall extends ParamsCallImpl {
131+
GrapeParamsCall() {
132+
exists(GrapeEndpoint endpoint |
133+
this.getParent+() = endpoint.getBody().asCallableAstNode() and
134+
this.getMethodName() = "params"
135+
)
136+
or
137+
// Also handle cases where params is called on an instance of a Grape API class
138+
this = grapeAPIInstance().getAMethodCall("params").asExpr().getExpr()
139+
}
140+
}
141+
142+
/**
143+
* A call to `headers` from within a Grape API endpoint.
144+
* Headers can also be a source of user input.
145+
*/
146+
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
147+
GrapeHeadersSource() {
148+
this.asExpr().getExpr() instanceof GrapeHeadersCall
149+
}
150+
151+
override string getSourceType() { result = "Grape::API#headers" }
152+
153+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
154+
}
155+
156+
/**
157+
* A call to `headers` from within a Grape API endpoint.
158+
*/
159+
private class GrapeHeadersCall extends MethodCall {
160+
GrapeHeadersCall() {
161+
exists(GrapeEndpoint endpoint |
162+
this.getParent+() = endpoint.getBody().asCallableAstNode() and
163+
this.getMethodName() = "headers"
164+
)
165+
or
166+
// Also handle cases where headers is called on an instance of a Grape API class
167+
this = grapeAPIInstance().getAMethodCall("headers").asExpr().getExpr()
168+
}
169+
}
170+
171+
/**
172+
* A call to `request` from within a Grape API endpoint.
173+
* The request object can contain user input.
174+
*/
175+
class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
176+
GrapeRequestSource() {
177+
this.asExpr().getExpr() instanceof GrapeRequestCall
178+
}
179+
180+
override string getSourceType() { result = "Grape::API#request" }
181+
182+
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
183+
}
184+
185+
/**
186+
* A call to `request` from within a Grape API endpoint.
187+
*/
188+
private class GrapeRequestCall extends MethodCall {
189+
GrapeRequestCall() {
190+
exists(GrapeEndpoint endpoint |
191+
this.getParent+() = endpoint.getBody().asCallableAstNode() and
192+
this.getMethodName() = "request"
193+
)
194+
or
195+
// Also handle cases where request is called on an instance of a Grape API class
196+
this = grapeAPIInstance().getAMethodCall("request").asExpr().getExpr()
197+
}
198+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
grapeAPIClasses
2+
| app.rb:1:1:48:3 | MyAPI |
3+
| app.rb:50:1:54:3 | AdminAPI |
4+
grapeEndpoints
5+
| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
6+
| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
7+
| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
8+
| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
9+
| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
10+
| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
11+
| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
12+
| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin |
13+
grapeParams
14+
| app.rb:8:12:8:17 | call to params |
15+
| app.rb:14:3:16:5 | call to params |
16+
| app.rb:18:11:18:16 | call to params |
17+
| app.rb:24:10:24:15 | call to params |
18+
| app.rb:31:5:31:10 | call to params |
19+
| app.rb:36:5:36:10 | call to params |
20+
| app.rb:52:5:52:10 | call to params |
21+
grapeHeaders
22+
| app.rb:9:18:9:24 | call to headers |
23+
| app.rb:46:5:46:11 | call to headers |
24+
grapeRequest
25+
| app.rb:25:12:25:18 | call to request |
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import ruby
2+
import codeql.ruby.frameworks.Grape
3+
import codeql.ruby.Concepts
4+
import codeql.ruby.AST
5+
6+
query predicate grapeAPIClasses(GrapeAPIClass api) { any() }
7+
8+
query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) {
9+
endpoint = api.getAnEndpoint() and
10+
method = endpoint.getHttpMethod() and
11+
path = endpoint.getPath()
12+
}
13+
14+
query predicate grapeParams(GrapeParamsSource params) { any() }
15+
16+
query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
17+
18+
query predicate grapeRequest(GrapeRequestSource request) { any() }
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class MyAPI < Grape::API
2+
version 'v1', using: :header, vendor: 'myapi'
3+
format :json
4+
prefix :api
5+
6+
desc 'Simple get endpoint'
7+
get '/hello/:name' do
8+
name = params[:name]
9+
user_agent = headers['User-Agent']
10+
"Hello #{name}!"
11+
end
12+
13+
desc 'Post endpoint with params'
14+
params do
15+
requires :message, type: String
16+
end
17+
post '/messages' do
18+
msg = params[:message]
19+
{ status: 'received', message: msg }
20+
end
21+
22+
desc 'Put endpoint accessing request'
23+
put '/update/:id' do
24+
id = params[:id]
25+
body = request.body.read
26+
{ id: id, body: body }
27+
end
28+
29+
desc 'Delete endpoint'
30+
delete '/items/:id' do
31+
params[:id]
32+
end
33+
34+
desc 'Patch endpoint'
35+
patch '/items/:id' do
36+
params[:id]
37+
end
38+
39+
desc 'Head endpoint'
40+
head '/status' do
41+
# Just return status
42+
end
43+
44+
desc 'Options endpoint'
45+
options '/info' do
46+
headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
47+
end
48+
end
49+
50+
class AdminAPI < Grape::API
51+
get '/admin' do
52+
params[:token]
53+
end
54+
end

ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,13 @@ def unsafe_action
66
sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
77
sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
88
end
9+
end
10+
11+
class PotatoAPI < Grape::API
12+
get '/unsafe_endpoint' do
13+
name = params[:user_name]
14+
# BAD: SQL statement constructed from user input
15+
sql = Arel.sql("SELECT * FROM users WHERE name = #{name}")
16+
sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}")
17+
end
918
end

ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ edges
8181
| ArelInjection.rb:4:5:4:8 | name | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
8282
| ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:4:12:4:29 | ...[...] | provenance | |
8383
| ArelInjection.rb:4:12:4:29 | ...[...] | ArelInjection.rb:4:5:4:8 | name | provenance | |
84+
| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
85+
| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep |
86+
| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | |
87+
| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | |
8488
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
8589
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
8690
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -209,6 +213,11 @@ nodes
209213
| ArelInjection.rb:4:12:4:29 | ...[...] | semmle.label | ...[...] |
210214
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
211215
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
216+
| ArelInjection.rb:13:5:13:8 | name | semmle.label | name |
217+
| ArelInjection.rb:13:12:13:17 | call to params | semmle.label | call to params |
218+
| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] |
219+
| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
220+
| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
212221
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
213222
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
214223
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -266,6 +275,8 @@ subpaths
266275
| ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | ActiveRecordInjection.rb:222:29:222:34 | call to params | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | This SQL query depends on a $@. | ActiveRecordInjection.rb:222:29:222:34 | call to params | user-provided value |
267276
| ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
268277
| ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value |
278+
| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
279+
| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value |
269280
| PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
270281
| PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
271282
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |

0 commit comments

Comments
 (0)