@@ -8,23 +8,233 @@ private import codeql.ruby.Concepts
88private import codeql.ruby.DataFlow
99private import codeql.ruby.dataflow.FlowSummary
1010private 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}
0 commit comments