Merge pull request #10090 from hmac/hmac/activestorage

Ruby: Model Activestorage
This commit is contained in:
Harry Maclean
2022-09-29 09:16:25 +13:00
committed by GitHub
7 changed files with 382 additions and 21 deletions

View File

@@ -0,0 +1,6 @@
---
category: minorAnalysis
---
* Various code executions, command executions and HTTP requests in the
ActiveStorage library are now recognized.

View File

@@ -528,13 +528,17 @@ private module Persistence {
* end
* ```
*/
private class ActiveRecordAssociation extends DataFlow::CallNode {
class ActiveRecordAssociation extends DataFlow::CallNode {
private ActiveRecordModelClass modelClass;
ActiveRecordAssociation() {
not exists(this.asExpr().getExpr().getEnclosingMethod()) and
this.asExpr().getExpr().getEnclosingModule() = modelClass and
this.getMethodName() = ["has_one", "has_many", "belongs_to", "has_and_belongs_to_many"]
this.getMethodName() =
[
"has_one", "has_many", "belongs_to", "has_and_belongs_to_many", "has_one_attached",
"has_many_attached"
]
}
/**
@@ -584,21 +588,32 @@ private class ActiveRecordAssociation extends DataFlow::CallNode {
}
/** Holds if this association is one-to-one */
predicate isSingular() { this.getMethodName() = ["has_one", "belongs_to"] }
predicate isSingular() { this.getMethodName() = ["has_one", "belongs_to", "has_one_attached"] }
/** Holds if this association is one-to-many or many-to-many */
predicate isCollection() { this.getMethodName() = ["has_many", "has_and_belongs_to_many"] }
predicate isCollection() {
this.getMethodName() = ["has_many", "has_and_belongs_to_many", "has_many_attached"]
}
}
/**
* Converts `input` to plural form.
*
* Examples:
*
* - photo -> photos
* - story -> stories
* - photos -> photos
*/
bindingset[input]
bindingset[result]
private string pluralize(string input) {
exists(string stem | stem + "y" = input | result = stem + "ies")
or
not exists(string stem | stem + "s" = input) and
result = input + "s"
or
exists(string stem | stem + "s" = input) and result = input
}
/**

View File

@@ -8,23 +8,218 @@ private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.frameworks.ActiveRecord
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
ActiveStorageFilenameSanitizedCall() {
this.getReceiver() =
API::getTopLevelMember("ActiveStorage").getMember("Filename").getAnInstantiation() and
this.getMethodName() = "sanitized"
}
}
/** Taint related to `ActiveStorage::Filename`. */
private class Summaries extends ModelInput::SummaryModelCsv {
override predicate row(string row) {
row =
[
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
]
/**
* Provides modeling for the `ActiveStorage` library.
* Version: 7.0.
*/
module ActiveStorage {
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
private class FilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
FilenameSanitizedCall() {
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Filename")
.getInstance()
.getAMethodCall("sanitized")
}
}
/** Taint related to `ActiveStorage::Filename`. */
private class FilenameSummaries extends ModelInput::SummaryModelCsv {
override predicate row(string row) {
row =
[
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
]
}
}
/**
* `Blob` is an instance of `ActiveStorage::Blob`.
*/
private class BlobTypeSummary extends ModelInput::TypeModelCsv {
override predicate row(string row) {
// package1;type1;package2;type2;path
row =
[
// ActiveStorage::Blob.new : Blob
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Instance",
// ActiveStorage::Blob.create_and_upload! : Blob
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[create_and_upload!].ReturnValue",
// ActiveStorage::Blob.create_before_direct_upload! : Blob
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[create_before_direct_upload!].ReturnValue",
// ActiveStorage::Blob.compose(blobs : [Blob]) : Blob
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[compose].ReturnValue",
// gives error: Invalid name 'Element' in access path
// "activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[compose].Argument[0].Element[any]",
// ActiveStorage::Blob.find_signed(!) : Blob
"activestorage;Blob;activestorage;;Member[ActiveStorage].Member[Blob].Method[find_signed,find_signed!].ReturnValue",
]
}
}
private class BlobInstance extends DataFlow::Node {
BlobInstance() {
this = ModelOutput::getATypeNode("activestorage", "Blob").getAValueReachableFromSource()
or
// ActiveStorage::Attachment#blob : Blob
exists(DataFlow::CallNode call |
call = this and
call.getReceiver() instanceof AttachmentInstance and
call.getMethodName() = "blob"
)
or
// ActiveStorage::Attachment delegates method calls to its associated Blob
this instanceof AttachmentInstance
}
}
/**
* Method calls on `ActiveStorage::Blob` that send HTTP requests.
*/
private class BlobRequestCall extends Http::Client::Request::Range {
BlobRequestCall() {
this =
[
// Class methods
API::getTopLevelMember("ActiveStorage")
.getMember("Blob")
.getASubclass()
.getAMethodCall(["create_after_unfurling!", "create_and_upload!"]),
// Instance methods
any(BlobInstance i, DataFlow::CallNode c |
i.(DataFlow::LocalSourceNode).flowsTo(c.getReceiver()) and
c.getMethodName() =
[
"upload", "upload_without_unfurling", "download", "download_chunk", "delete",
"purge"
]
|
c
)
]
}
override string getFramework() { result = "activestorage" }
override DataFlow::Node getResponseBody() { result = this }
override DataFlow::Node getAUrlPart() { none() }
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
none()
}
}
/**
* A call to `has_one_attached` or `has_many_attached`, which declares an
* association between an ActiveRecord model and an ActiveStorage attachment.
*
* ```rb
* class User < ActiveRecord::Base
* has_one_attached :avatar
* end
* ```
*/
private class Association extends ActiveRecordAssociation {
Association() { this.getMethodName() = ["has_one_attached", "has_many_attached"] }
}
/**
* An ActiveStorage attachment, instantiated directly or via an association with an
* ActiveRecord model.
*
* ```rb
* class User < ActiveRecord::Base
* has_one_attached :avatar
* end
*
* user = User.find(id)
* user.avatar
* ActiveStorage::Attachment.new
* ```
*/
class AttachmentInstance extends DataFlow::Node {
AttachmentInstance() {
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Attachment")
.getInstance()
.getAValueReachableFromSource()
or
exists(Association assoc, string model, DataFlow::CallNode call |
model = assoc.getTargetModelName()
|
call = this and
call.getReceiver().(ActiveRecordInstance).getClass() = assoc.getSourceClass() and
call.getMethodName() = model
)
or
any(AttachmentInstance i).(DataFlow::LocalSourceNode).flowsTo(this)
}
}
/**
* A call on an ActiveStorage object that results in an image transformation.
* Arguments to these calls may be executed as system commands.
*/
private class ImageProcessingCall extends DataFlow::CallNode, SystemCommandExecution::Range {
ImageProcessingCall() {
this.getReceiver() instanceof BlobInstance and
this.getMethodName() = ["variant", "preview", "representation"]
or
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Attachment")
.getInstance()
.getAMethodCall(["variant", "preview", "representation"])
or
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Variation")
.getAMethodCall(["new", "wrap", "encode"])
or
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Variation")
.getInstance()
.getAMethodCall("transformations=")
or
this =
API::getTopLevelMember("ActiveStorage")
.getMember("Transformers")
.getMember("ImageProcessingTransformer")
.getAMethodCall("new")
or
this =
API::getTopLevelMember("ActiveStorage")
.getMember(["Preview", "VariantWithRecord"])
.getAMethodCall("new")
or
// `ActiveStorage.paths` is a global hash whose values are passed to
// a `system` call.
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("paths=")
or
// `ActiveStorage.video_preview_arguments` is passed to a `system` call.
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("video_preview_arguments=")
}
override DataFlow::Node getAnArgument() { result = this.getArgument(0) }
}
/**
* `ActiveStorage.variant_processor` is passed to `const_get`.
*/
private class VariantProcessor extends DataFlow::CallNode, CodeExecution::Range {
VariantProcessor() {
this = API::getTopLevelMember("ActiveStorage").getAMethodCall("variant_processor=")
}
override DataFlow::Node getCode() { result = this.getArgument(0) }
}
}

View File

@@ -3,6 +3,7 @@ actionControllerControllerClasses
| active_record/ActiveRecord.rb:41:1:64:3 | BarController |
| active_record/ActiveRecord.rb:66:1:94:3 | BazController |
| active_record/ActiveRecord.rb:96:1:104:3 | AnnotatedController |
| active_storage/active_storage.rb:39:1:45:3 | PostsController |
| app/controllers/comments_controller.rb:1:1:7:3 | CommentsController |
| app/controllers/foo/bars_controller.rb:3:1:46:3 | BarsController |
| app/controllers/photos_controller.rb:1:1:4:3 | PhotosController |
@@ -21,6 +22,7 @@ actionControllerActionMethods
| active_record/ActiveRecord.rb:91:3:93:5 | update3 |
| active_record/ActiveRecord.rb:97:3:99:5 | index |
| active_record/ActiveRecord.rb:101:3:103:5 | unsafe_action |
| active_storage/active_storage.rb:40:3:44:5 | create |
| app/controllers/comments_controller.rb:2:3:3:5 | index |
| app/controllers/comments_controller.rb:5:3:6:5 | show |
| app/controllers/foo/bars_controller.rb:5:3:7:5 | index |
@@ -59,6 +61,8 @@ paramsCalls
| active_record/ActiveRecord.rb:92:28:92:33 | call to params |
| active_record/ActiveRecord.rb:92:53:92:58 | call to params |
| active_record/ActiveRecord.rb:102:59:102:64 | call to params |
| active_storage/active_storage.rb:41:21:41:26 | call to params |
| active_storage/active_storage.rb:42:24:42:29 | call to params |
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |
@@ -89,6 +93,8 @@ paramsSources
| active_record/ActiveRecord.rb:92:28:92:33 | call to params |
| active_record/ActiveRecord.rb:92:53:92:58 | call to params |
| active_record/ActiveRecord.rb:102:59:102:64 | call to params |
| active_storage/active_storage.rb:41:21:41:26 | call to params |
| active_storage/active_storage.rb:42:24:42:29 | call to params |
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |

View File

@@ -0,0 +1,47 @@
attachmentInstances
| active_storage.rb:11:1:11:25 | ... = ... |
| active_storage.rb:11:1:11:25 | ... = ... |
| active_storage.rb:11:15:11:25 | call to avatar |
| active_storage.rb:13:1:13:11 | user_avatar |
| active_storage.rb:14:1:14:11 | user_avatar |
| active_storage.rb:15:1:15:11 | user_avatar |
| active_storage.rb:17:1:17:11 | call to avatar |
| active_storage.rb:19:1:19:42 | ... = ... |
| active_storage.rb:19:1:19:42 | ... = ... |
| active_storage.rb:19:14:19:42 | call to new |
| active_storage.rb:23:11:23:20 | attachment |
| active_storage.rb:24:11:24:20 | attachment |
| active_storage.rb:25:18:25:27 | attachment |
| active_storage.rb:42:5:42:15 | call to images |
| active_storage.rb:73:1:73:10 | attachment |
| active_storage.rb:74:1:74:10 | attachment |
httpRequests
| 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! |
| 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! |
| active_storage.rb:53:1:53:11 | call to upload | activestorage | active_storage.rb:53:1:53:11 | call to upload |
| 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 |
| active_storage.rb:55:1:55:13 | call to download | activestorage | active_storage.rb:55:1:55:13 | call to download |
| active_storage.rb:56:1:56:19 | call to download_chunk | activestorage | active_storage.rb:56:1:56:19 | call to download_chunk |
| active_storage.rb:57:1:57:11 | call to delete | activestorage | active_storage.rb:57:1:57:11 | call to delete |
| active_storage.rb:58:1:58:10 | call to purge | activestorage | active_storage.rb:58:1:58:10 | call to purge |
| active_storage.rb:61:1:61:11 | call to upload | activestorage | active_storage.rb:61:1:61:11 | call to upload |
| active_storage.rb:65:1:65:11 | call to upload | activestorage | active_storage.rb:65:1:65:11 | call to upload |
| active_storage.rb:68:1:68:11 | call to upload | activestorage | active_storage.rb:68:1:68:11 | call to upload |
| active_storage.rb:71:1:71:11 | call to upload | activestorage | active_storage.rb:71:1:71:11 | call to upload |
| active_storage.rb:73:1:73:22 | call to upload | activestorage | active_storage.rb:73:1:73:22 | call to upload |
| active_storage.rb:74:1:74:17 | call to upload | activestorage | active_storage.rb:74:1:74:17 | call to upload |
commandExecutions
| active_storage.rb:17:1:17:48 | call to variant | active_storage.rb:17:21:17:47 | Pair |
| active_storage.rb:23:11:23:57 | call to variant | active_storage.rb:23:30:23:56 | Pair |
| active_storage.rb:24:11:24:44 | call to preview | active_storage.rb:24:30:24:43 | Pair |
| active_storage.rb:25:18:25:59 | call to representation | active_storage.rb:25:44:25:58 | Pair |
| active_storage.rb:28:1:28:25 | call to transformations= | active_storage.rb:28:29:28:43 | ... = ... |
| active_storage.rb:30:15:30:90 | call to new | active_storage.rb:30:75:30:89 | transformations |
| active_storage.rb:31:11:31:53 | call to new | active_storage.rb:31:38:31:52 | transformations |
| active_storage.rb:32:11:32:63 | call to new | active_storage.rb:32:48:32:62 | transformations |
| active_storage.rb:34:1:34:19 | call to paths= | active_storage.rb:34:23:34:60 | ... = ... |
| active_storage.rb:35:1:35:37 | call to video_preview_arguments= | active_storage.rb:35:41:35:59 | ... = ... |
codeExecutions
| active_storage.rb:37:1:37:31 | call to variant_processor= | active_storage.rb:37:35:37:50 | ... = ... |
pathSanitizations
| active_storage.rb:48:1:48:18 | call to sanitized |

View File

@@ -0,0 +1,18 @@
import ruby
import codeql.ruby.ApiGraphs
import codeql.ruby.Concepts
import codeql.ruby.frameworks.ActiveStorage
query predicate attachmentInstances(ActiveStorage::AttachmentInstance n) { any() }
query predicate httpRequests(Http::Client::Request r, string framework, DataFlow::Node responseBody) {
r.getFramework() = framework and r.getResponseBody() = responseBody
}
query predicate commandExecutions(SystemCommandExecution c, DataFlow::Node arg) {
arg = c.getAnArgument()
}
query predicate codeExecutions(CodeExecution e, DataFlow::Node code) { code = e.getCode() }
query predicate pathSanitizations(Path::PathSanitization p) { any() }

View File

@@ -0,0 +1,74 @@
class User < ActiveRecord::Base
has_one_attached :avatar
end
class Post < ActiveRecord::Base
has_many_attached :images
end
user = User.find(id)
user_avatar = user.avatar
user_avatar.preview
user_avatar.representation
user_avatar.variant
user.avatar.variant(resize_to_limit: [128, 128])
attachment = ActiveStorage::Attachment.new
transformations = [{ resize_to_limit: [128, 128] }, { gaussblur: 3 }]
variant = attachment.variant(resize_to_limit: [128, 128])
preview = attachment.preview(gaussblur: 0.3)
representation = attachment.representation(crop: "300x300")
variation = ActiveStorage::Variation.new
variation.transformations = transformations
transformer = ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
preview = ActiveStorage::Preview.new(transformations)
variant = ActiveStorage::VariantWithRecord.new(transformations)
ActiveStorage.paths = { minimagick: custom_minimagick_path }
ActiveStorage.video_preview_arguments = custom_preview_args
ActiveStorage.variant_processor = custom_processor
class PostsController < ActionController::Base
def create
post = Post.new(params[:post])
post.images.attach(params[:images])
post.save
end
end
filename = ActiveStorage::Filename.new(raw_path)
filename.sanitized
ActiveStorage::Blob.create_after_unfurling!(io: file, filename: "foo.jpg")
blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: "foo.jpg")
blob.upload
blob.upload_without_unfurling
blob.download
blob.download_chunk
blob.delete
blob.purge
blob = ActiveStorage::Blob.create_before_direct_upload!(io: file, filename: "foo.jpg")
blob.upload
blob = ActiveStorage::Blob.compose([blob1, blob2])
blob1.upload # not recognised currently
blob.upload
blob = ActiveStorage::Blob.find_signed(id)
blob.upload
blob = ActiveStorage::Blob.find_signed!(id)
blob.upload
attachment.blob.upload
attachment.upload