New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pass service_name param to DirectUploadsController #38957
Pass service_name param to DirectUploadsController #38957
Conversation
384e95c
to
aac15a9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable to me (Granted I’m not a rails contributor or qualified to approve it)
@georgeclaghorn may I ask you to take a look at this issue? 🙂 |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
It's still an issue |
I would like to see this merged, my project is currently affected by #38940 and this seems to resolve it! |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Still an issue |
I just noticed this issue and was about to submit a PR for it. Thank you for doing this. I really need this functionality, and it'd be half-baked if this were not also included in the 6.1 release. Can someone please review? |
@georgeclaghorn May I ask you for a review? I guess many people will be happy to see it in the next release 🙂 |
activestorage/app/controllers/active_storage/direct_uploads_controller.rb
Show resolved
Hide resolved
hey @gmcgibbon -- sorry to bug you, but i saw you helped review the original pr that introduced this great feature. maybe you could help review this fix for it. |
@artisr did you run the upgrade migrations? 🙂 |
attachment_reflection = options[:object].class.reflect_on_attachment(name) | ||
|
||
if service_name = attachment_reflection.options[:service_name] | ||
options["data-direct-upload-service"] = ActiveStorage.verifier.generate(service_name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Encrypting the service name is good, but it is still susceptible to injection in other forms. Since ActiveStorage.verifier.generate("something") == ActiveStorage.verifier.generate("something")
is always true, the client doesn't know what the value is, but they could save the encrypted service token and send it in another request meant for another service.
You can mitigate the potential damage a client could do by adding an expires_in
option to the generate
call. This has serious downsides though.
- The client is now time-boxed to submitting the direct upload.
- The client can still use the token with expiry for other forms/requests while time permits.
I can't think of a clear workaround for this, but the ideal solution would key the encrypted service token to the form it is meant for. Sort of like how per_form_csrf_tokens
works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping in mind the option to use expires_in
, I've spent some trying to figure out as a form unique identifier and how to access it from #file_field_tag
without passing any additional options to it.
attachment_reflection
, attachment_name
and service_name
are available in the context. I've tried to build a key as a #{attachment_name}##{service_name}
, but there is no way to check that attachment_name
matches token in the DirectUploadsController#create
. I guess I could do some validations at the moment when Attachment
is created (e.g., add the validation to the Attachment
to make sure, that record.class.reflect_on_attachment(name).options[:service_name]
is nil or equals to Blob#service_name
) but it sounds brittle.
Any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could encrypt the following:
{ class_name: options[:object].class, association_name: name }.to_json
and then in the controller you would decrypt and constantize the class to get the service_name from the current attachment reflection. This effectively allows the client to select the attachment, and not the service_name, but that is just be pushing the problem one level deeper. The token with attachment association metadata could still be injected in other forms, but you are limited to only services in use.
Validation on attachment to match the blob service_name with the one in the attachment reflection sounds like a good idea, but might be addressing the problem at the wrong time (the blob is already persisted and uploaded).
Right now this feels like this is an impossible problem. As soon as we give the client a multiple tokens for a general uploading endpoint (to create a blob that is meant for a specific attachment), we have a problem. The controller and/or models will likely have to change more considerably in order to support this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've committed an attempt to solve this (sorry, it took a bit longer than I expected). This is how I expect it to work (most part of token generation code was shamelessly stolen from forgery protection module):
- We generate token based on
session[:_direct_upload_token]
(exactly the same way as CSRF token works) and include service name, class name and attachment name (service name can be decrypted later). - We add service_name and token to the form input and submit them along with upload.
- In the upload controller we decrypt token and make sure it matches token we expect (based on input data attributes, so it's impossible to use another user's token)
- If everything is fine we extract service_name from token and believe that it's a correct service_name
The only way to abuse this token is to use it to upload the same attachment of the same entity to the different form (which sounds like a very rare scenario). Everything is in the separate commit to make reviewing easier, I'll squash them later if we decide that this approach works. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gmcgibbon @georgeclaghorn may I ask you to take a look at this one more time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why go through the effort of encrypting the service name? Am I not right in assuming that the service_name
is included in the response body for DirectUploadsController#create
anyways? So it's already exposed to the client?
0491c58
to
30ec67c
Compare
dd14d5c
to
9f65882
Compare
@dhh would you mind weighing in here? This looks to me like an important bug in Rails 6.1, because people could be uploading files to the incorrect service without noticing. |
c45caee
to
d0465ea
Compare
Would these javascript examples need to be updated in the guide as well since the function definition changed with this? https://github.com/rails/rails/blob/main/guides/source/active_storage_overview.md?plain=1#L1153 |
Good point! Updated docs |
ac9a9ab
to
81c040b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM after squashing your changes 👍
Thanks for your patience on this ❤️
@@ -506,14 +506,16 @@ var activestorage = {exports: {}}; | |||
} | |||
} | |||
class BlobRecord { | |||
constructor(file, checksum, url) { | |||
constructor(file, checksum, url, directUploadToken, attachmentName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not entirely sure what the rules are regarding changing public JavaScript API, but the amount of positional parameters here is too much. We should make this a object/hash, but we can figure this later. Same with DirectUpload.
81c040b
to
29a5cdc
Compare
29a5cdc
to
193289d
Compare
Congrats on getting this merged in! I've been following this for 2 years, and I am so happy to see the persistence pay off One day when I get a chance, I can go remove my monkey patches in old code that worked around this 🎉 |
This doesn't jive well with Turbo. We allow users to leave notes on customer profiles and support for tagging users with @mentions with ActionText, so the form includes a @DmitryTsepelev, it seems that this PR generates tokens with the session to now support multiple upload services, however, there won't be a session for |
@DmitryTsepelev Can you revert this for 7.0.2? This is busted and we're nearing the one month mark after missing a fix for 7.0.1. |
Summary
While working on configuring individual services per attachment (#34935), I completely forgot about Direct Uploads (thanks @scottjg for finding it early in #38940). The problem is that during direct uploading blob is created without connection with the attachment (and record), so we cannot figure out, what service was configured, which leads to the situation when blob is uploaded to the default storage.
Other Information
I think we could solve it by passing
data-direct-upload-service
to the form input and submitting it to theDirectUploadsController#create
. It's not safe to let the client know what services we have configured, so service name is hidden usingActiveStorage.verifier.generate
.Please let me know if there is any easy way to access
record
during blob creation or if my approach is not secure enough 🙂