Skip to content

Commit 53a3417

Browse files
committed
Model ActiveStorage
1 parent 39a1cf5 commit 53a3417

File tree

5 files changed

+390
-20
lines changed

5 files changed

+390
-20
lines changed

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -528,13 +528,17 @@ private module Persistence {
528528
* end
529529
* ```
530530
*/
531-
private class ActiveRecordAssociation extends DataFlow::CallNode {
531+
class ActiveRecordAssociation extends DataFlow::CallNode {
532532
private ActiveRecordModelClass modelClass;
533533

534534
ActiveRecordAssociation() {
535535
not exists(this.asExpr().getExpr().getEnclosingMethod()) and
536536
this.asExpr().getExpr().getEnclosingModule() = modelClass and
537-
this.getMethodName() = ["has_one", "has_many", "belongs_to", "has_and_belongs_to_many"]
537+
this.getMethodName() =
538+
[
539+
"has_one", "has_many", "belongs_to", "has_and_belongs_to_many", "has_one_attached",
540+
"has_many_attached"
541+
]
538542
}
539543

540544
/**
@@ -584,21 +588,32 @@ private class ActiveRecordAssociation extends DataFlow::CallNode {
584588
}
585589

586590
/** Holds if this association is one-to-one */
587-
predicate isSingular() { this.getMethodName() = ["has_one", "belongs_to"] }
591+
predicate isSingular() { this.getMethodName() = ["has_one", "belongs_to", "has_one_attached"] }
588592

589593
/** Holds if this association is one-to-many or many-to-many */
590-
predicate isCollection() { this.getMethodName() = ["has_many", "has_and_belongs_to_many"] }
594+
predicate isCollection() {
595+
this.getMethodName() = ["has_many", "has_and_belongs_to_many", "has_many_attached"]
596+
}
591597
}
592598

593599
/**
594600
* Converts `input` to plural form.
601+
*
602+
* Examples:
603+
*
604+
* - photo -> photos
605+
* - story -> stories
606+
* - photos -> photos
595607
*/
596608
bindingset[input]
597609
bindingset[result]
598-
private string pluralize(string input) {
610+
string pluralize(string input) {
599611
exists(string stem | stem + "y" = input | result = stem + "ies")
600612
or
613+
not exists(string stem | stem + "s" = input) and
601614
result = input + "s"
615+
or
616+
exists(string stem | stem + "s" = input) and result = input
602617
}
603618

604619
/**

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

Lines changed: 225 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,233 @@ private import codeql.ruby.Concepts
88
private import codeql.ruby.DataFlow
99
private import codeql.ruby.dataflow.FlowSummary
1010
private import codeql.ruby.frameworks.data.ModelsAsData
11+
private import codeql.ruby.frameworks.ActiveRecord
1112

12-
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
13-
class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
14-
ActiveStorageFilenameSanitizedCall() {
15-
this.getReceiver() =
16-
API::getTopLevelMember("ActiveStorage").getMember("Filename").getAnInstantiation() and
17-
this.getMethodName() = "sanitized"
13+
module ActiveStorage {
14+
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
15+
private class FilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
16+
FilenameSanitizedCall() {
17+
this =
18+
API::getTopLevelMember("ActiveStorage")
19+
.getMember("Filename")
20+
.getInstance()
21+
.getAMethodCall("sanitized")
22+
}
1823
}
19-
}
2024

21-
/** Taint related to `ActiveStorage::Filename`. */
22-
private class Summaries extends ModelInput::SummaryModelCsv {
23-
override predicate row(string row) {
24-
row =
25-
[
26-
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
27-
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
28-
]
25+
/** Taint related to `ActiveStorage::Filename`. */
26+
private class FilenameSummaries extends ModelInput::SummaryModelCsv {
27+
override predicate row(string row) {
28+
row =
29+
[
30+
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
31+
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
32+
]
33+
}
34+
}
35+
36+
/**
37+
* `Blob` is an instance of `ActiveStorage::Blob`.
38+
*/
39+
private class BlobTypeSummary extends ModelInput::TypeModelCsv {
40+
override predicate row(string row) {
41+
// package1;type1;package2;type2;path
42+
row =
43+
[
44+
// ActiveStorage::Blob.new : Blob
45+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Instance",
46+
// ActiveStorage::Blob.create_and_upload! : Blob
47+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[create_and_upload!].ReturnValue",
48+
// ActiveStorage::Blob.create_before_direct_upload! : Blob
49+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[create_before_direct_upload!].ReturnValue",
50+
// ActiveStorage::Blob.compose(blobs : [Blob]) : Blob
51+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[compose].ReturnValue",
52+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[compose].Argument[0].Element[any]",
53+
// ActiveStorage::Blob.find_signed(!) : Blob
54+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[find_signed,find_signed!].ReturnValue",
55+
// ActiveStorage::Attachment#blob : Blob
56+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Attachment].Instance.Method[blob].ReturnValue",
57+
// ActiveStorage::Attachment delegates method calls to its associated Blob
58+
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Attachment].Instance",
59+
]
60+
}
61+
}
62+
63+
/**
64+
* Method calls on `ActiveStorage::Blob` that send HTTP requests.
65+
*/
66+
private class BlobRequestCall extends HTTP::Client::Request::Range {
67+
BlobRequestCall() {
68+
this =
69+
[
70+
// Class methods
71+
API::getTopLevelMember("ActiveStorage")
72+
.getMember("Blob")
73+
.getASubclass*()
74+
.getAMethodCall(["create_after_unfurling!", "create_and_upload!"]),
75+
// Instance methods
76+
ModelOutput::getATypeNode("activestorage", "Blob")
77+
.getAMethodCall([
78+
"upload", "upload_without_unfurling", "download", "download_chunk", "delete",
79+
"purge"
80+
])
81+
].asExpr().getExpr()
82+
}
83+
84+
override string getFramework() { result = "activestorage" }
85+
86+
override DataFlow::Node getResponseBody() { result.asExpr().getExpr() = this }
87+
88+
override DataFlow::Node getAUrlPart() { none() }
89+
90+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) { none() }
91+
}
92+
93+
/**
94+
* A call to `has_one_attached` or `has_many_attached`, which declares an
95+
* association between an ActiveRecord model and an ActiveStorage attachment.
96+
*
97+
* ```rb
98+
* class User < ActiveRecord::Base
99+
* has_one_attached :avatar
100+
* end
101+
* ```
102+
*/
103+
private class Association extends ActiveRecordAssociation {
104+
Association() { this.getMethodName() = ["has_one_attached", "has_many_attached"] }
105+
}
106+
107+
/**
108+
* An ActiveStorage attachment, instantiated via an association with an
109+
* ActiveRecord model.
110+
*
111+
* ```rb
112+
* class User < ActiveRecord::Base
113+
* has_one_attached :avatar
114+
* end
115+
*
116+
* user = User.find(id)
117+
* user.avatar
118+
* ```
119+
*/
120+
private class AttachmentInstance extends DataFlow::CallNode {
121+
Association assoc;
122+
123+
AttachmentInstance() {
124+
exists(string model | model = assoc.getTargetModelName() |
125+
this.getReceiver().(ActiveRecordInstance).getClass() = assoc.getSourceClass() and
126+
(
127+
assoc.isSingular() and this.getMethodName() = model
128+
or
129+
assoc.isCollection() and this.getMethodName() = model
130+
)
131+
)
132+
}
133+
}
134+
135+
/**
136+
* A call on an ActiveStorage object that results in an image transformation.
137+
* Arguments to these calls may be executed as system commands.
138+
*/
139+
private class ImageProcessingCall extends DataFlow::CallNode, SystemCommandExecution::Range {
140+
ImageProcessingCall() {
141+
this =
142+
ModelOutput::getATypeNode("activestorage", "Blob")
143+
.getAMethodCall(["variant", "preview", "representation"]) or
144+
this =
145+
API::getTopLevelMember("ActiveStorage")
146+
.getMember("Attachment")
147+
.getInstance()
148+
.getAMethodCall(["variant", "preview", "representation"]) or
149+
this =
150+
API::getTopLevelMember("ActiveStorage")
151+
.getMember("Variation")
152+
.getAMethodCall(["new", "wrap", "encode"]) or
153+
this =
154+
API::getTopLevelMember("ActiveStorage")
155+
.getMember("Variation")
156+
.getInstance()
157+
.getAMethodCall("transformations=") or
158+
this =
159+
API::getTopLevelMember("ActiveStorage")
160+
.getMember("Transformers")
161+
.getMember("ImageProcessingTransformer")
162+
.getAMethodCall("new") or
163+
this =
164+
API::getTopLevelMember("ActiveStorage")
165+
.getMember(["Preview", "VariantWithRecord"])
166+
.getAMethodCall("new") or
167+
// `ActiveStorage.paths` is a global hash whose values are passed to
168+
// a `system` call.
169+
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("paths=") or
170+
// `ActiveStorage.video_preview_arguments` is passed to a `system` call.
171+
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("video_preview_arguments=")
172+
}
173+
174+
override DataFlow::Node getAnArgument() { result = this.getArgument(0) }
175+
}
176+
177+
/**
178+
* `ActiveStorage.variant_processor` is passed to `const_get`.
179+
*/
180+
private class VariantProcessor extends DataFlow::CallNode, CodeExecution::Range {
181+
VariantProcessor() {
182+
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("variant_processor=")
183+
}
184+
185+
override DataFlow::Node getCode() { result = this.getArgument(0) }
186+
}
187+
188+
/**
189+
* Adds ActiveStorage instances to the API graph.
190+
* Source code may not mention `ActiveStorage` or `ActiveStorage::Attachment`,
191+
* so we add synthetic nodes for them.
192+
*/
193+
private module ApiNodes {
194+
class ActiveStorage extends API::EntryPoint {
195+
ActiveStorage() { this = "ActiveStorage" }
196+
197+
override predicate edge(API::Node pred, API::Label::ApiLabel lbl) {
198+
pred = API::root() and lbl = API::Label::member("ActiveStorage")
199+
}
200+
}
201+
202+
class Attachment extends API::EntryPoint {
203+
Attachment() { this = "ActiveStorage::Attachment" }
204+
205+
override predicate edge(API::Node pred, API::Label::ApiLabel lbl) {
206+
pred = API::getTopLevelMember("ActiveStorage") and
207+
lbl = API::Label::member("Attachment")
208+
}
209+
}
210+
211+
class AttachmentNew extends API::EntryPoint {
212+
AttachmentNew() { this = "ActiveStorage::Attachment.new" }
213+
214+
override predicate edge(API::Node pred, API::Label::ApiLabel lbl) {
215+
pred = API::getTopLevelMember("ActiveStorage").getMember("Attachment") and
216+
lbl = API::Label::method("new")
217+
}
218+
}
219+
220+
/**
221+
* An API entry point for instances of `ActiveStorage::Attachment`.
222+
* These arise from calls to methods generated by `has_one_attached` and
223+
* `has_many_attached` associations.
224+
*/
225+
class AttachmentInstanceNode extends API::EntryPoint {
226+
AttachmentInstanceNode() { this = "ActiveStorage::Attachment.new.ReturnValue" }
227+
228+
override predicate edge(API::Node pred, API::Label::ApiLabel lbl) {
229+
pred = API::getTopLevelMember("ActiveStorage").getMember("Attachment").getMethod("new") and
230+
lbl = API::Label::return()
231+
}
232+
233+
override DataFlow::LocalSourceNode getAUse() { result = any(AttachmentInstance i) }
234+
235+
override DataFlow::CallNode getACall() {
236+
any(AttachmentInstance i).flowsTo(result.getReceiver())
237+
}
238+
}
29239
}
30240
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
attachmentInstances
2+
| active_storage.rb:11:1:11:25 | ... = ... |
3+
| active_storage.rb:11:1:11:25 | ... = ... |
4+
| active_storage.rb:11:15:11:25 | call to avatar |
5+
| active_storage.rb:13:1:13:11 | user_avatar |
6+
| active_storage.rb:14:1:14:11 | user_avatar |
7+
| active_storage.rb:15:1:15:11 | user_avatar |
8+
| active_storage.rb:17:1:17:11 | call to avatar |
9+
| active_storage.rb:19:1:19:42 | ... = ... |
10+
| active_storage.rb:19:1:19:42 | ... = ... |
11+
| active_storage.rb:19:14:19:42 | call to new |
12+
| active_storage.rb:23:11:23:20 | attachment |
13+
| active_storage.rb:24:11:24:20 | attachment |
14+
| active_storage.rb:25:18:25:27 | attachment |
15+
| active_storage.rb:42:5:42:15 | call to images |
16+
| active_storage.rb:73:1:73:10 | attachment |
17+
| active_storage.rb:74:1:74:10 | attachment |
18+
httpRequests
19+
| active_storage.rb:50:1:50:74 | call to create_after_unfurling! | activestorage | active_storage.rb:50:1:50:74 | call to create_after_unfurling! |
20+
| active_storage.rb:51:8:51:76 | call to create_and_upload! | activestorage | active_storage.rb:51:8:51:76 | call to create_and_upload! |
21+
| active_storage.rb:53:1:53:11 | call to upload | activestorage | active_storage.rb:53:1:53:11 | call to upload |
22+
| active_storage.rb:54:1:54:29 | call to upload_without_unfurling | activestorage | active_storage.rb:54:1:54:29 | call to upload_without_unfurling |
23+
| active_storage.rb:55:1:55:13 | call to download | activestorage | active_storage.rb:55:1:55:13 | call to download |
24+
| active_storage.rb:56:1:56:19 | call to download_chunk | activestorage | active_storage.rb:56:1:56:19 | call to download_chunk |
25+
| active_storage.rb:57:1:57:11 | call to delete | activestorage | active_storage.rb:57:1:57:11 | call to delete |
26+
| active_storage.rb:58:1:58:10 | call to purge | activestorage | active_storage.rb:58:1:58:10 | call to purge |
27+
| active_storage.rb:61:1:61:11 | call to upload | activestorage | active_storage.rb:61:1:61:11 | call to upload |
28+
| active_storage.rb:65:1:65:11 | call to upload | activestorage | active_storage.rb:65:1:65:11 | call to upload |
29+
| active_storage.rb:68:1:68:11 | call to upload | activestorage | active_storage.rb:68:1:68:11 | call to upload |
30+
| active_storage.rb:71:1:71:11 | call to upload | activestorage | active_storage.rb:71:1:71:11 | call to upload |
31+
| active_storage.rb:73:1:73:22 | call to upload | activestorage | active_storage.rb:73:1:73:22 | call to upload |
32+
| active_storage.rb:74:1:74:17 | call to upload | activestorage | active_storage.rb:74:1:74:17 | call to upload |
33+
commandExecutions
34+
| active_storage.rb:17:1:17:48 | call to variant | active_storage.rb:17:21:17:47 | Pair |
35+
| active_storage.rb:23:11:23:57 | call to variant | active_storage.rb:23:30:23:56 | Pair |
36+
| active_storage.rb:24:11:24:44 | call to preview | active_storage.rb:24:30:24:43 | Pair |
37+
| active_storage.rb:25:18:25:59 | call to representation | active_storage.rb:25:44:25:58 | Pair |
38+
| active_storage.rb:28:1:28:25 | call to transformations= | active_storage.rb:28:29:28:43 | ... = ... |
39+
| active_storage.rb:30:15:30:90 | call to new | active_storage.rb:30:75:30:89 | transformations |
40+
| active_storage.rb:31:11:31:53 | call to new | active_storage.rb:31:38:31:52 | transformations |
41+
| active_storage.rb:32:11:32:63 | call to new | active_storage.rb:32:48:32:62 | transformations |
42+
| active_storage.rb:34:1:34:19 | call to paths= | active_storage.rb:34:23:34:60 | ... = ... |
43+
| active_storage.rb:35:1:35:37 | call to video_preview_arguments= | active_storage.rb:35:41:35:59 | ... = ... |
44+
codeExecutions
45+
| active_storage.rb:37:1:37:31 | call to variant_processor= | active_storage.rb:37:35:37:50 | ... = ... |
46+
pathSanitizations
47+
| active_storage.rb:48:1:48:18 | call to sanitized |
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import ruby
2+
import codeql.ruby.ApiGraphs
3+
import codeql.ruby.DataFlow
4+
import codeql.ruby.Concepts
5+
6+
query predicate attachmentInstances(DataFlow::Node n) {
7+
n =
8+
API::getTopLevelMember("ActiveStorage")
9+
.getMember("Attachment")
10+
.getInstance()
11+
.getAValueReachableFromSource()
12+
}
13+
14+
query predicate httpRequests(HTTP::Client::Request r, string framework, DataFlow::Node responseBody) {
15+
r.getFramework() = framework and r.getResponseBody() = responseBody
16+
}
17+
18+
query predicate commandExecutions(SystemCommandExecution c, DataFlow::Node arg) {
19+
arg = c.getAnArgument()
20+
}
21+
22+
query predicate codeExecutions(CodeExecution e, DataFlow::Node code) { code = e.getCode() }
23+
24+
query predicate pathSanitizations(Path::PathSanitization p) { any() }

0 commit comments

Comments
 (0)