Skip to content

Commit 774030a

Browse files
authored
Merge pull request github#12083 from pwntester/ruby_twirp_support
[Ruby] Add support for Twirp framework
2 parents 52dd1f4 + 74782bf commit 774030a

File tree

11 files changed

+205
-0
lines changed

11 files changed

+205
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Support for [Twirp framework](https://twitchtv.github.io/twirp/docs/intro.html).

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ 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.Twirp
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Provides classes for modeling the `Twirp` framework.
3+
*/
4+
5+
private import codeql.ruby.DataFlow
6+
private import codeql.ruby.CFG
7+
private import codeql.ruby.ApiGraphs
8+
private import codeql.ruby.AST as Ast
9+
private import codeql.ruby.security.ServerSideRequestForgeryCustomizations
10+
private import codeql.ruby.Concepts
11+
12+
/**
13+
* Provides classes for modeling the `Twirp` framework.
14+
*/
15+
module Twirp {
16+
/**
17+
* A Twirp service instantiation
18+
*/
19+
class ServiceInstantiation extends DataFlow::CallNode {
20+
ServiceInstantiation() {
21+
this =
22+
API::getTopLevelMember("Twirp").getMember("Service").getASubclass().getAnInstantiation()
23+
}
24+
25+
/**
26+
* Gets a local source node for the Service instantiation argument (the service handler).
27+
*/
28+
private DataFlow::LocalSourceNode getHandlerSource() {
29+
result = this.getArgument(0).getALocalSource()
30+
}
31+
32+
/**
33+
* Gets the API::Node for the service handler's class.
34+
*/
35+
private API::Node getAHandlerClassApiNode() {
36+
result.getAnInstantiation() = this.getHandlerSource()
37+
}
38+
39+
/**
40+
* Gets the AST module for the service handler's class.
41+
*/
42+
private Ast::Module getAHandlerClassAstNode() {
43+
result =
44+
this.getAHandlerClassApiNode()
45+
.asSource()
46+
.asExpr()
47+
.(CfgNodes::ExprNodes::ConstantReadAccessCfgNode)
48+
.getExpr()
49+
.getModule()
50+
}
51+
52+
/**
53+
* Gets a handler's method.
54+
*/
55+
Ast::Method getAHandlerMethod() {
56+
result = this.getAHandlerClassAstNode().getAnInstanceMethod()
57+
}
58+
}
59+
60+
/**
61+
* A Twirp client
62+
*/
63+
class ClientInstantiation extends DataFlow::CallNode {
64+
ClientInstantiation() {
65+
this = API::getTopLevelMember("Twirp").getMember("Client").getASubclass().getAnInstantiation()
66+
}
67+
}
68+
69+
/** The URL of a Twirp service, considered as a sink. */
70+
class ServiceUrlAsSsrfSink extends ServerSideRequestForgery::Sink {
71+
ServiceUrlAsSsrfSink() { exists(ClientInstantiation c | c.getArgument(0) = this) }
72+
}
73+
74+
/** A parameter that will receive parts of the url when handling an incoming request. */
75+
class UnmarshaledParameter extends Http::Server::RequestInputAccess::Range,
76+
DataFlow::ParameterNode {
77+
UnmarshaledParameter() {
78+
exists(ServiceInstantiation i | i.getAHandlerMethod().getParameter(0) = this.asParameter())
79+
}
80+
81+
override string getSourceType() { result = "Twirp Unmarhaled Parameter" }
82+
83+
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
84+
}
85+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source "https://rubygems.org"
2+
3+
gem "rack"
4+
gem "webrick"
5+
gem "twirp"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sourceTest
2+
| hello_world_server.rb:8:13:8:15 | req |
3+
ssrfSinkTest
4+
| hello_world_client.rb:6:47:6:75 | "http://localhost:8080/twirp" |
5+
serviceInstantiationTest
6+
| hello_world_server.rb:24:11:24:61 | call to new |
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
private import codeql.ruby.frameworks.Twirp
2+
private import codeql.ruby.DataFlow
3+
4+
query predicate sourceTest(Twirp::UnmarshaledParameter source) { any() }
5+
6+
query predicate ssrfSinkTest(Twirp::ServiceUrlAsSsrfSink sink) { any() }
7+
8+
query predicate serviceInstantiationTest(Twirp::ServiceInstantiation si) { any() }
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
syntax = "proto3";
2+
package example.hello_world;
3+
4+
5+
service HelloWorld {
6+
rpc Hello(HelloRequest) returns (HelloResponse);
7+
}
8+
9+
message HelloRequest {
10+
string name = 1;
11+
}
12+
13+
message HelloResponse {
14+
string message = 1;
15+
}

ruby/ql/test/library-tests/frameworks/Twirp/hello_world/service_pb.rb

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Code generated by protoc-gen-twirp_ruby 1.10.0, DO NOT EDIT.
2+
require 'twirp'
3+
require_relative 'service_pb.rb'
4+
5+
module Example
6+
module HelloWorld
7+
class HelloWorldService < ::Twirp::Service
8+
package 'example.hello_world'
9+
service 'HelloWorld'
10+
rpc :Hello, HelloRequest, HelloResponse, :ruby_method => :hello
11+
end
12+
13+
class HelloWorldClient < ::Twirp::Client
14+
client_for HelloWorldService
15+
end
16+
end
17+
end
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'rack'
2+
3+
require_relative 'hello_world/service_twirp.rb'
4+
5+
# test: ssrfSink
6+
c = Example::HelloWorld::HelloWorldClient.new("http://localhost:8080/twirp")
7+
8+
resp = c.hello(name: "World")
9+
if resp.error
10+
puts resp.error
11+
else
12+
puts resp.data.message
13+
end

0 commit comments

Comments
 (0)