Add support for route parameters(+ blocks), headers, and cookies in Grape API

This commit is contained in:
Chad Bentz
2025-09-12 22:51:47 -04:00
parent 3252bd39d2
commit 5cfa6e83b3
6 changed files with 239 additions and 23 deletions

View File

@@ -137,12 +137,14 @@ private class GrapeParamsCall extends ParamsCallImpl {
)
}
}/**
* A call to `headers` from within a Grape API endpoint.
* A call to `headers` from within a Grape API endpoint or headers block.
* Headers can also be a source of user input.
*/
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
GrapeHeadersSource() {
this.asExpr().getExpr() instanceof GrapeHeadersCall
or
this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
}
override string getSourceType() { result = "Grape::API#headers" }
@@ -179,6 +181,20 @@ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
}
/**
* A call to `route_param` from within a Grape API endpoint.
* Route parameters are extracted from the URL path and can be a source of user input.
*/
class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
GrapeRouteParamSource() {
this.asExpr().getExpr() instanceof GrapeRouteParamCall
}
override string getSourceType() { result = "Grape::API#route_param" }
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
}
/**
* A call to `request` from within a Grape API endpoint.
*/
@@ -194,6 +210,80 @@ private class GrapeRequestCall extends MethodCall {
}
}
/**
* A call to `route_param` from within a Grape API endpoint.
*/
private class GrapeRouteParamCall extends MethodCall {
GrapeRouteParamCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asExpr().getExpr() and
this.getMethodName() = "route_param"
)
or
// Also handle cases where route_param is called on an instance of a Grape API class
this = grapeAPIInstance().getAMethodCall("route_param").asExpr().getExpr()
}
}
/**
* A call to `headers` block within a Grape API class.
* This is different from the headers() method call - this is the DSL block for defining header requirements.
*/
private class GrapeHeadersBlockCall extends MethodCall {
GrapeHeadersBlockCall() {
exists(GrapeAPIClass api |
this.getParent+() = api.getADeclaration() and
this.getMethodName() = "headers" and
exists(this.getBlock())
)
}
}
/**
* A call to `cookies` block within a Grape API class.
* This DSL block defines cookie requirements and those cookies are user-controlled.
*/
private class GrapeCookiesBlockCall extends MethodCall {
GrapeCookiesBlockCall() {
exists(GrapeAPIClass api |
this.getParent+() = api.getADeclaration() and
this.getMethodName() = "cookies" and
exists(this.getBlock())
)
}
}
/**
* A call to `cookies` method from within a Grape API endpoint or cookies block.
* Similar to headers, cookies can be accessed as a method and are user-controlled input.
*/
class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
GrapeCookiesSource() {
this.asExpr().getExpr() instanceof GrapeCookiesCall
or
this.asExpr().getExpr() instanceof GrapeCookiesBlockCall
}
override string getSourceType() { result = "Grape::API#cookies" }
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
}
/**
* A call to `cookies` method from within a Grape API endpoint.
*/
private class GrapeCookiesCall extends MethodCall {
GrapeCookiesCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asCallableAstNode() and
this.getMethodName() = "cookies"
)
or
// Also handle cases where cookies is called on an instance of a Grape API class
this = grapeAPIInstance().getAMethodCall("cookies").asExpr().getExpr()
}
}
/**
* A method defined within a `helpers` block in a Grape API class.
* These methods become available in endpoint contexts through Grape's DSL.

View File

@@ -1,15 +1,18 @@
grapeAPIClasses
| app.rb:1:1:48:3 | MyAPI |
| app.rb:50:1:54:3 | AdminAPI |
| app.rb:1:1:90:3 | MyAPI |
| app.rb:92:1:96:3 | AdminAPI |
grapeEndpoints
| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin |
| app.rb:1:1:90:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
| app.rb:1:1:90:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
| app.rb:1:1:90:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
| app.rb:1:1:90:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
| app.rb:1:1:90:3 | MyAPI | app.rb:50:3:54:5 | call to get | GET | /users/:user_id/posts/:post_id |
| app.rb:1:1:90:3 | MyAPI | app.rb:78:3:82:5 | call to get | GET | /cookie_test |
| app.rb:1:1:90:3 | MyAPI | app.rb:85:3:89:5 | call to get | GET | /header_test |
| app.rb:92:1:96:3 | AdminAPI | app.rb:93:3:95:5 | call to get | GET | /admin |
grapeParams
| app.rb:8:12:8:17 | call to params |
| app.rb:14:3:16:5 | call to params |
@@ -17,9 +20,21 @@ grapeParams
| app.rb:24:10:24:15 | call to params |
| app.rb:31:5:31:10 | call to params |
| app.rb:36:5:36:10 | call to params |
| app.rb:52:5:52:10 | call to params |
| app.rb:60:12:60:17 | call to params |
| app.rb:94:5:94:10 | call to params |
grapeHeaders
| app.rb:9:18:9:24 | call to headers |
| app.rb:46:5:46:11 | call to headers |
| app.rb:66:3:69:5 | call to headers |
| app.rb:86:12:86:18 | call to headers |
| app.rb:87:14:87:20 | call to headers |
grapeRequest
| app.rb:25:12:25:18 | call to request |
grapeRouteParam
| app.rb:51:15:51:35 | call to route_param |
| app.rb:52:15:52:36 | call to route_param |
| app.rb:57:3:63:5 | call to route_param |
grapeCookies
| app.rb:72:3:75:5 | call to cookies |
| app.rb:79:15:79:21 | call to cookies |
| app.rb:80:16:80:22 | call to cookies |

View File

@@ -15,4 +15,8 @@ query predicate grapeParams(GrapeParamsSource params) { any() }
query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
query predicate grapeRequest(GrapeRequestSource request) { any() }
query predicate grapeRequest(GrapeRequestSource request) { any() }
query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() }
query predicate grapeCookies(GrapeCookiesSource cookies) { any() }

View File

@@ -45,6 +45,48 @@ class MyAPI < Grape::API
options '/info' do
headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
end
desc 'Route param endpoint'
get '/users/:user_id/posts/:post_id' do
user_id = route_param(:user_id)
post_id = route_param('post_id')
{ user_id: user_id, post_id: post_id }
end
desc 'Route param block pattern'
route_param :id do
get do
# params[:id] is user input from the path parameter
id = params[:id]
{ id: id }
end
end
# Headers block for defining expected headers
headers do
requires :Authorization, type: String
optional 'X-Custom-Header', type: String
end
# Cookies block for defining expected cookies
cookies do
requires :session_id, type: String
optional :tracking_id, type: String
end
desc 'Endpoint that uses cookies method'
get '/cookie_test' do
session = cookies[:session_id]
tracking = cookies['tracking_id']
{ session: session, tracking: tracking }
end
desc 'Endpoint that uses headers method'
get '/header_test' do
auth = headers[:Authorization]
custom = headers['X-Custom-Header']
{ auth: auth, custom: custom }
end
end
class AdminAPI < Grape::API

View File

@@ -33,9 +33,39 @@ end
end
end
# Headers and cookies blocks for DSL testing
headers do
requires :Authorization, type: String
end
cookies do
requires :session_id, type: String
end
get '/comprehensive_test/:user_id' do
# BAD: Comprehensive test using all Grape input sources in one SQL query
user_id = params[:user_id] # params taint source
route_id = route_param(:user_id) # route_param taint source
auth = headers[:Authorization] # headers taint source
session = cookies[:session_id] # cookies taint source
body_data = request.body.read # request taint source
# All sources flow to SQL injection
Arel.sql("SELECT * FROM users WHERE id = #{user_id} AND route_id = #{route_id} AND auth = #{auth} AND session = #{session} AND data = #{body_data}")
end
get '/helper_test' do
# This should be detected as SQL injection via helper method
# BAD: Test helper method dataflow
user_id = params[:user_id]
vulnerable_helper(user_id)
end
# Test route_param block pattern
route_param :id do
get do
# BAD: params[:id] should be user input from the path
user_id = params[:id]
Arel.sql("SELECT * FROM users WHERE id = #{user_id}")
end
end
end

View File

@@ -89,10 +89,24 @@ edges
| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | |
| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | |
| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:38:7:38:13 | user_id | ArelInjection.rb:39:25:39:31 | user_id | provenance | |
| ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:38:17:38:32 | ...[...] | provenance | |
| ArelInjection.rb:38:17:38:32 | ...[...] | ArelInjection.rb:38:7:38:13 | user_id | provenance | |
| ArelInjection.rb:39:25:39:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
| ArelInjection.rb:47:7:47:13 | user_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:47:17:47:32 | ...[...] | provenance | |
| ArelInjection.rb:47:17:47:32 | ...[...] | ArelInjection.rb:47:7:47:13 | user_id | provenance | |
| ArelInjection.rb:48:7:48:14 | route_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:48:7:48:14 | route_id | provenance | |
| ArelInjection.rb:49:7:49:10 | auth | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:49:14:49:36 | ...[...] | provenance | |
| ArelInjection.rb:49:14:49:36 | ...[...] | ArelInjection.rb:49:7:49:10 | auth | provenance | |
| ArelInjection.rb:50:7:50:13 | session | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:50:17:50:36 | ...[...] | provenance | |
| ArelInjection.rb:50:17:50:36 | ...[...] | ArelInjection.rb:50:7:50:13 | session | provenance | |
| ArelInjection.rb:59:7:59:13 | user_id | ArelInjection.rb:60:25:60:31 | user_id | provenance | |
| ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:59:17:59:32 | ...[...] | provenance | |
| ArelInjection.rb:59:17:59:32 | ...[...] | ArelInjection.rb:59:7:59:13 | user_id | provenance | |
| ArelInjection.rb:60:25:60:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep |
| ArelInjection.rb:67:9:67:15 | user_id | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep |
| ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:67:19:67:29 | ...[...] | provenance | |
| ArelInjection.rb:67:19:67:29 | ...[...] | ArelInjection.rb:67:9:67:15 | user_id | provenance | |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep |
| PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep |
@@ -232,10 +246,26 @@ nodes
| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." |
| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id |
| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
| ArelInjection.rb:38:7:38:13 | user_id | semmle.label | user_id |
| ArelInjection.rb:38:17:38:22 | call to params | semmle.label | call to params |
| ArelInjection.rb:38:17:38:32 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:39:25:39:31 | user_id | semmle.label | user_id |
| ArelInjection.rb:47:7:47:13 | user_id | semmle.label | user_id |
| ArelInjection.rb:47:17:47:22 | call to params | semmle.label | call to params |
| ArelInjection.rb:47:17:47:32 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:48:7:48:14 | route_id | semmle.label | route_id |
| ArelInjection.rb:48:18:48:38 | call to route_param | semmle.label | call to route_param |
| ArelInjection.rb:49:7:49:10 | auth | semmle.label | auth |
| ArelInjection.rb:49:14:49:20 | call to headers | semmle.label | call to headers |
| ArelInjection.rb:49:14:49:36 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:50:7:50:13 | session | semmle.label | session |
| ArelInjection.rb:50:17:50:23 | call to cookies | semmle.label | call to cookies |
| ArelInjection.rb:50:17:50:36 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
| ArelInjection.rb:59:7:59:13 | user_id | semmle.label | user_id |
| ArelInjection.rb:59:17:59:22 | call to params | semmle.label | call to params |
| ArelInjection.rb:59:17:59:32 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:60:25:60:31 | user_id | semmle.label | user_id |
| ArelInjection.rb:67:9:67:15 | user_id | semmle.label | user_id |
| ArelInjection.rb:67:19:67:24 | call to params | semmle.label | call to params |
| ArelInjection.rb:67:19:67:29 | ...[...] | semmle.label | ...[...] |
| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." |
| PgInjection.rb:6:5:6:8 | name | semmle.label | name |
| PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params |
| PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] |
@@ -296,7 +326,12 @@ subpaths
| 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 |
| 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 |
| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value |
| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:38:17:38:22 | call to params | user-provided value |
| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:59:17:59:22 | call to params | user-provided value |
| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:47:17:47:22 | call to params | user-provided value |
| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:48:18:48:38 | call to route_param | user-provided value |
| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:49:14:49:20 | call to headers | user-provided value |
| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:50:17:50:23 | call to cookies | user-provided value |
| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:67:19:67:24 | call to params | user-provided value |
| 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 |
| 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 |
| 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 |