mirror of
https://github.com/github/codeql.git
synced 2026-04-28 10:15:14 +02:00
Merge pull request #10090 from hmac/hmac/activestorage
Ruby: Model Activestorage
This commit is contained in:
6
ruby/ql/lib/change-notes/2022-08-30-activestorage.md
Normal file
6
ruby/ql/lib/change-notes/2022-08-30-activestorage.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Various code executions, command executions and HTTP requests in the
|
||||
ActiveStorage library are now recognized.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
@@ -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() }
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user