Skip to content
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

Merged

Conversation

DmitryTsepelev
Copy link
Contributor

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 the DirectUploadsController#create. It's not safe to let the client know what services we have configured, so service name is hidden using ActiveStorage.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 🙂

Copy link

@scottjg scottjg left a 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)

@DmitryTsepelev
Copy link
Contributor Author

@georgeclaghorn may I ask you to take a look at this issue? 🙂

@rails-bot
Copy link

rails-bot bot commented Jul 27, 2020

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.
Thank you for your contributions.

@rails-bot rails-bot bot added the stale label Jul 27, 2020
@DmitryTsepelev
Copy link
Contributor Author

It's still an issue

@rails-bot rails-bot bot removed the stale label Jul 28, 2020
@neiljohari
Copy link

neiljohari commented Jul 29, 2020

I would like to see this merged, my project is currently affected by #38940 and this seems to resolve it!

@rails-bot
Copy link

rails-bot bot commented Oct 27, 2020

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.
Thank you for your contributions.

@rails-bot rails-bot bot added the stale label Oct 27, 2020
@neiljohari
Copy link

Still an issue

@rails-bot rails-bot bot removed the stale label Oct 27, 2020
@adeeb1
Copy link

adeeb1 commented Nov 2, 2020

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?

@DmitryTsepelev
Copy link
Contributor Author

@georgeclaghorn May I ask you for a review? I guess many people will be happy to see it in the next release 🙂

@scottjg
Copy link

scottjg commented Nov 21, 2020

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.

@gmcgibbon
Copy link
Member

This looks like a more complete version of #37501. @javan had concerns over service name injection, so we may have to refactor the approach slightly. I'll give this some more thought. Thank you for re-raising this, we really should support multi-service in direct uploads.

@gmcgibbon gmcgibbon self-requested a review November 21, 2020 06:01
@DmitryTsepelev
Copy link
Contributor Author

@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)
Copy link
Member

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.

  1. The client is now time-boxed to submitting the direct upload.
  2. 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.

Copy link
Contributor Author

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_nameare 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?

Copy link
Member

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.

Copy link
Contributor Author

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):

  1. 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).
  2. We add service_name and token to the form input and submit them along with upload.
  3. 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)
  4. 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?

Copy link
Contributor Author

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?

Copy link

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?

@DmitryTsepelev @gmcgibbon

@DmitryTsepelev DmitryTsepelev force-pushed the store-per-attachment-direct-upload branch 3 times, most recently from 0491c58 to 30ec67c Compare January 13, 2021 13:54
Base automatically changed from master to main January 14, 2021 17:01
@DmitryTsepelev DmitryTsepelev force-pushed the store-per-attachment-direct-upload branch from dd14d5c to 9f65882 Compare January 28, 2021 09:14
@santib
Copy link
Contributor

santib commented Feb 14, 2021

@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.

@t27duck
Copy link
Contributor

t27duck commented Nov 4, 2021

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
https://github.com/rails/rails/blob/main/guides/source/active_storage_overview.md?plain=1#L1182

@rails-bot rails-bot bot added the docs label Nov 8, 2021
@DmitryTsepelev
Copy link
Contributor Author

Good point! Updated docs

@DmitryTsepelev DmitryTsepelev force-pushed the store-per-attachment-direct-upload branch from ac9a9ab to 81c040b Compare November 8, 2021 20:22
Copy link
Member

@gmcgibbon gmcgibbon left a 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) {
Copy link
Member

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.

@DmitryTsepelev DmitryTsepelev force-pushed the store-per-attachment-direct-upload branch from 81c040b to 29a5cdc Compare November 14, 2021 08:31
@DmitryTsepelev DmitryTsepelev force-pushed the store-per-attachment-direct-upload branch from 29a5cdc to 193289d Compare November 14, 2021 08:49
@gmcgibbon gmcgibbon merged commit fb36712 into rails:main Nov 15, 2021
@neiljohari
Copy link

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 🎉

@tbcooney
Copy link

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 rich_text_field. When a new note is created, it'll trigger the call to broadcast_later which enqueues a Turbo::Streams::ActionBroadcastJob for the prepend.

@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 Turbo::Streams::ActionBroadcastJob, and this raises a fatal error. Check out the trace in the photo below,

Screen Shot 2021-12-20 at 1 51 08 PM

@tbcooney
Copy link

@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.

@DmitryTsepelev
Copy link
Contributor Author

@tbcooney I cannot revert anything since I'm just a commiter 🙂 What I can do is to implement the temp solution described here, but I failed to fit it into my last week

@gmcgibbon gmcgibbon mentioned this pull request Jan 28, 2022
dhh pushed a commit that referenced this pull request Jan 29, 2022
* Revert "Pass service_name param to DirectUploadsController"

This reverts commit 193289d.

* Revert "Multi-service direct uploads in Action Text attachment uploads"

This reverts commit 0b69ad4.
dhh pushed a commit that referenced this pull request Jan 29, 2022
* Revert "Pass service_name param to DirectUploadsController"

This reverts commit 193289d.

* Revert "Multi-service direct uploads in Action Text attachment uploads"

This reverts commit 0b69ad4.
public-rant pushed a commit to opensource-rant/rails that referenced this pull request Feb 17, 2022
* Revert "Pass service_name param to DirectUploadsController"

This reverts commit 193289d.

* Revert "Multi-service direct uploads in Action Text attachment uploads"

This reverts commit 0b69ad4.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet