Skip to content
This repository was archived by the owner on Apr 16, 2024. It is now read-only.

Conversation

@torss
Copy link
Collaborator

@torss torss commented Jun 26, 2018

Description:

Closes #729

This secures the 'uploads' route, which was previously completely unprotected / public.

Due to browser API limitations it proved to be apparently impossible to modify the request headers for <img>s or <video>s, meaning that we can't simply attach our JWT localStorage['token'] as we do for various other API requests. And while it is possible to separately download image files via BackendServices getDownload or similar, this is work-around is not feasible for serving large videos! Thus a different approach was required:

A new, specialized 'mediaToken' is generated in addition to the regular 'token'. This 'mediaToken' is only granted access to routes using the accompanying new passportJwtMiddlewareMedia (that means the underlying new passportJwtStrategyMedia). So if the 'mediaToken' is accidentally or willfully passed on by a user, the recipient can't use it to fully impersonate the user / can't use it to gain access to the actual geli page functionality that requires the regular 'token'. This is important because this new 'mediaToken' is transmitted in form of a URL query parameter.

This solution means that <img>s and more importantly <video>s are now secured without compromising functionality (e.g. by always separately pre-loading entire files). There is also a beneficial side-effect to attaching the 'mediaToken' as URL query parameter: Users can still copy the file-URLs with their attached 'mediaToken' and send them to anyone else. Recipients of such URLs can still view the file, but only as long as the 'mediaToken's last. Since we apparently already employ randomized (unguessable) filenames, there is no major risk of abusing 'mediaToken's to gain access to files that have not already been published in some way. Nevertheless, this is not perfect and sharing is currently not really intended as a result, see Known Issues for more.

Improvements

  • Secured the static 'uploads' route by means of the approach described above.
  • Added a JwtPipe to easily append 'mediaToken's to file URLs.
  • Added a FileComponent to flexibly display a single file e.g. in a new tab. It is currently used for the Edit courseMediaOpen in new Tab functionality. (This was originally intended as an unfortunate necessity to separately download files via BackendServices getDownload - which, again, would have been bad for large videos - but it now has become a simply more flexible, Angular-controllable form to display a single file versus just opening the file-URL in a new tab.)
  • Appended 'mediaToken' to various file URLs via JwtPipe and adapted front-end code in general.
  • Replaced hard-coded 'upload' strings in the API with config.uploadFolder.
  • Fixed missing @UseBefore middleware in MediaController.
  • Secured DownloadControllergetArchivedFileid input usage.
  • Fixed picture.path backslash issue / regression. This doesn't really have anything to do with issue 🐛 BUG: Fix security leak: static upload path #729, the problem just came up after the merge. See this commit for details.
  • Other unrelated small improvements (refactoring, fixes, unit tests), e.g. better DownloadController coverage via new tests with new DeleteCache admin-only API.
  • Admin front-end control for DeleteCache, housed in a new 'Misc.' admin sub-component.
  • (Scrapped experiment of a @Controller-based replacement for the static 'uploads' route: UploadsController. See Known Issues for more.)

Known Issues:

  • The URLs become quite long by attaching a 'mediaToken'. This however should be no significant issue, since they are mostly invisible to the user (e.g. unless a user chooses to Copy image address or similar). Opening a new tab via Angular can route to the new FileComponent, which tries to gracefully embed the real requested file.
  • No differentiation between different users has been achieved, so once a user knows the secret filename of a course he doesn't have access to, he can access that file with his own token indefinitely. This was meant to be solved via the custom UploadsController in place of the express.static system, but while that controller seemingly worked, it caused problems for video requests that I could not solve so far without modifying the routing-controllers library. Perhaps a viable future alternative would be to use express directly to achieve this, although this wouldn't be too elegant in comparison to everything else using the routing-controllers.
  • Concerns have been raised in the mpse-geli/dev Gitter room about storing the tokens in localStorage due to potential XSS attacks. This is not directly related to this issue 🐛 BUG: Fix security leak: static upload path #729, but if we should switch to using cookies for authentication (which must then prevent CSRF attacks and would require pervasive changes), then we would no longer have to transmit the 'mediaToken' as URL query parameter as well.
  • I got waaay to many strong headaches from working on this issue because of all the seemingly unsolvable roadblocks I encountered here, among other things. ;)

Possible superior solution for the future:

A better, more robust general solution for the future (independent of the mentioned potential switch to cookies above) might be to just remove the entire 'mediaToken' system again, and instead generate random proxy-filenames for each individual file-listing request. One drawback of this would be temporary server-side storage to associate the proxy-filenames with the real paths. But perhaps this too could be mitigated by generating JWTs per file, using them directly as file ID and relying on the JWT verification for security. Then the only drawback might be computational inefficiency due to the high number of JWTs that need to be generated.

torss added 30 commits June 4, 2018 17:28
This by itself secures the static 'uploads' route, but it of course
also breaks all benign access that doesn't send an authentication token
within the request header.
This is a prototype that works but still needs some polishing.
See the various added FIXMEs.
This now makes the getUserImageURL function of User.ts superfluous.
See FIXME in User.ts for implications of this.
Additionally the authenticated user-image.directive.ts now works mostly
as intended, in contrast to the prototype variant prior to this commit.
Note the FIXME in token.interceptor.ts, i.e.:
The Angular HttpInterceptor does not help with securing requests
made directly by the browser for <img> or <video> tags, so this
TokenInterceptor is probably useless and can/should be removed
before creating the PR for this branch/issue #729, if I am not mistaken.
Now passport.authenticate also includes assignProperty: 'jwtData'.
This uses the 'jwtData' specified via passport.authenticate's
'assignProperty' option in the passportJwtMiddlewareFactory.
The actual data with the 'tokenPayload' is created in the
passportJwtStrategyFactory. CurrentUserDecorator and RoleAuthorization
have been somewhat simplified as a result of this change.
Using '@UseBefore(passportJwtMiddleware)' is now functionally required
due to the changes in CurrentUserDecorator and RoleAuthorization since
the 'jwtData' must be set. (Although this probably should have been
added to MediaController.ts regardless.)
@torss
Copy link
Collaborator Author

torss commented Jul 17, 2018

@torss you can take your time, I will look after my exams on Thursday after it.

@PatrickSkowronek What's your status?

@PatrickSkowronek
Copy link
Collaborator

@torss I'm at right now 😄

Copy link
Collaborator

@PatrickSkowronek PatrickSkowronek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 Big error I found, the other is a new Issue!

* {}
*/
@Delete('/cache')
@Authorized(['admin'])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this function, but is it usable in the Frontend?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, and I only wrote that route because I wanted to improve the DownloadController tests, which I in turn only did because of the reduced coverage. (So that was by design & doesn't really have anything to do with the PR.) But I concur, it would be nice to have that functionality be accessible from the admin front-end; will try to add that now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


transform(url: string, type: string = 'media'): string {
const key = this.availableTypes[type];
const token = localStorage[key].split(' ')[1];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be undefined , which is on the first login. We need to catch the undefined error!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, good catch! I completely missed that.

The actual problem was not the JwtPipe - the localStorage[key] there must never be undefined when the transform gets called, so it should throw an error and extra undefined-handling code is not needed. Instead, the real problem was that JwtPipe got used before login completion in the first place! This happened because of changes in one of the merges, i.e. the ones re. PR #751 with the actualProfilePicturePath stuff I already had somewhat annoying issues with before, as mentioned in some of the comments/commits above.
(I think I should open a GitHub issue for refactoring that thing.)
Parts of that (i.e. updateProfilePicture) get called in the UserService's setUser, which got called in the login method of our AuthenticationService before the tokens were set, thus causing the undefined error. More precisely, the error occurs in the anonymous AppComponent function for userService.data.subscribe, and so it still occurred even without setUser, since that function gets called on initialization anyway, but didn't sufficiently check for empty (string) input.

And the reason for why I didn't notice this problem at all is because of another issue (this time one for which I, and not the merged stuff, is to blame ^^): I simply forgot to add code that clears the mediaToken on logout to the AuthenticationService! Ha.

torss added 10 commits July 18, 2018 15:20
The code to handle mediaTokens analogous to the regular token was
missing in the AuthenticationService constructor and unsetAuthData
method, the latter resulting in the persistence / leak of the
mediaToken after logging out!
Erroneously returned profile.path instead of profile.picture.path.
Calling userService.setUser before the mediaToken is set caused an
issue due to setUser currently calling updateProfilePicture, thus
causing a "chain reaction" that ultimately leads to the AppComponent
calling the jwtPipe.transform method, which fails without mediaToken.

This also removes a superfluous separate call to updateProfilePicture,
which was presumably added originally because of the previously broken
checkNewProfilePicturePath UserService method used in setUser.
Adding CHANGELOG lines for such small fixes is probably borderline
nonsensical, but there already is another typo entry, so I guess
I will add this for now. Maybe all the very small changes should
be grouped in the future?
Copy link
Collaborator

@PatrickSkowronek PatrickSkowronek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sry Mate, I found Something!

<picture class="responsive-image">
<source *ngFor="let breakpoint of this.responsiveImageData.breakpoints" media="(min-width: {{breakpoint.screenSize}}px)"
srcset="api/{{breakpoint.pathToImage}}{{breakpoint.pathToRetinaImage ? ' 1x, ' + breakpoint.pathToRetinaImage + ' 2x' : ''}}">
srcset="api/{{breakpoint.pathToImage | jwt}}{{breakpoint.pathToRetinaImage ? ' 1x, api/' + (breakpoint.pathToRetinaImage | jwt) + ' 2x' : ''}}">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This route is not working properly! The old route does still work. I added a new picture and it does not work anymore.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think this must have been broken by this.
Will fix it ASAP - After Sleeping Again :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind, the commit I previously referred to wasn't the issue - it did in fact provide the solution! The actual problem discovered here is that the ResponsiveImageService didn't account for absolute directory paths. (Which also is what was fixed in said commit, but for user profile pictures.)
Should be fixed now!

Plus minor refactoring to use path.join.
@PatrickSkowronek PatrickSkowronek merged commit d9e68ba into develop Jul 29, 2018
@torss torss added 🔒 security This directly pertains to geli's security! bug This Issue describes a unwanted behavior api All Backend related Issues labels Oct 11, 2018
@kesselb kesselb deleted the bugfix/#729-secure-static-uploads branch October 17, 2018 18:23
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

api All Backend related Issues bug This Issue describes a unwanted behavior 🔒 security This directly pertains to geli's security!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 BUG: Fix security leak: static upload path

6 participants