0

I have a File database table with the following columns:

Id (PK), Filename

And a Document database table with the following columns:

Id (PK), Name, Description, FileId (FK)

When a user wants to "save" a Document, my client-side JavaScript does it in two quick AJAX calls (in an attempt to be RESTful and save each resource individually):

  1. Uploads the file they have selected. This will create a File record in the database, and the client will receive its Id in response.

  2. Saves the Document with the given data (Name, FileId, etc.). This will create/update a Document record in the database.

I have a service set up that allows a user to download a file, provided they specify its Id. However, there is some security logic in the service that checks whether a user has permission to download the file, by checking whether there is at least one database "entity" associated with it that the user has access to. For example, a Document would be one such entity. So if a user can access a particular Document (determined by some business rules - but let's just say multiple users can access the same Document), they can download its file.

This all seems fine on the surface, but there is a problem. The 2-step save process for a Document is all client-side Javascript, so a hacker could easily modify step 2 so that any arbitrary FileId is sent to the server. This would then associate that file with the Document, meaning afterwards the user will have permission to download it!

What are some ways I can solve this problem?

  • Why does the visible REST interface need to show the internal DB structure? – Christopher Creutzig Jan 11 '14 at 14:18
  • It doesn't, but having a single endpoint for uploading files means I don't need to implement the server-side logic to process file uploads in multiple places. Also, it is useful if I need "preview" functionality (e.g. allowing the client to preview an uploaded image file before "saving" it with an association to a particular entity). – XåpplI'-I0llwlg'I - Jan 11 '14 at 14:28
  • I agree with Kevin below – this seems to me like you expose too much of your server implementation details to a different module, which is not entirely under your own control. His approaches are both good, I’d just like to add the question: Is your system in a long-term consistent state if the client aborts after the first Ajax call? – Christopher Creutzig Jan 11 '14 at 14:50
  • I believe it's Azure Blob Storage that has a feature that lets you generate a link that's only valid for X minutes, and the user can then upload to this link. In your case, after the user POST's the first request to save the document, the server could respond with a complicated and shortlived upload URL. This means hackers can't just upload to "any" document ID, because they'd need the custom URL to upload. (Added as a comment, because I don't know enough to give a complete answer, but this might point you in the right direction) – Falgantil Apr 9 at 7:59
1

This is what server-side state is for. One correct way to approach this is to record, in a server-side session state collection, the recently-uploaded files' Id values, and to restrict the SaveADocument operation to using a FileId from that collection (and, of course, removing it from the collection upon success). As a side effect, your session-cleanup logic now will have a definitive list of orphaned files, which you can delete at session cleanup.

  • This solution is similar to Kevin's in that the server has to store some state, but no encrypted file id needs to be used by the client. Nice! – XåpplI'-I0llwlg'I - Jan 11 '14 at 15:41
3

Couple different options as I see it

  1. Does this really need to be 2 calls from the client for 1 workflow?
  2. Instead of returning the id by itself, return an encrypted version of the id that can be validated in the second call (say the key is stored in the users session on the server)

Response to comment:

If you want to provide a 'preview' mode, my recommendation is an action that takes a file from the client (javascript cannot access the local file system) and writes it to disk, or stores it in session on the server-side and returns a view to the user with that item embedded for confirmation. Until they 'confirm' the preview I don't see the need to write any data about the file to the database as it does not need to be persisted. By writing to disk or saving in session state (only if it's small) you avoid the user having to re-upload the file and you can re-access it and the temporary storage is cheaper than the database storage on their confirmation.

I don't have a full view of the scenario so I'm only making educated guesses as to your intent

  • To address 1, see my response to the comment above, I'd be interested to hear your thoughts on it. As for 2, I really like that solution! I'm going to leave this question open a bit longer to get more viewpoints, but if no one chimes in I will accept this answer. – XåpplI'-I0llwlg'I - Jan 11 '14 at 14:41
  • I read your response and I didn't understand it; why not have 1 server side function that orchestrates each of the server side functions that would have been called independently by your client? If a preview function is needed wouldn't you just be writing the uploaded image to disk temporarily and avoiding the database altogether? – Kevin Jan 11 '14 at 14:55
  • I'm not sure I see the benefit of uploading to a temporary location. When the user wants to associate the file with another entity, do they have to re-upload the entire file again to its proper location? Or does the server move it to a proper location? How is that better than having a single RESTful endpoint for file uploads that stores files in their proper non-temporary location immediately, and also handles all server-side file upload logic so you don't have to do it anywhere else? – XåpplI'-I0llwlg'I - Jan 11 '14 at 15:21
  • In my expereince, normally you would move not reupload. Having an uploaded temporary location lets you do the necessary malware, size and other checks before moving it to the permanent location. – Elin Jan 11 '14 at 20:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.