Protect static photo URLs with signing #51

Open
opened 2024-10-04 03:58:33 +00:00 by noah · 0 comments
Owner

Currently, the actual file names of the .jpg images under /static/photos are not authenticated: if you get the direct link to one, you can always grab it even if not logged in. The site mainly relies on photo filenames to be randomized and unguessable.

With NGINX ngx_http_auth_request we may be able to protect these files and validate them against a logged-in user.

How it works

On the NGINX side, you configure the /static/ folder to do a subrequest for authentication:

# Static file access
location /static/ {
	auth_request /static-auth;
	alias /home/www-data/git/website/web/static/;
	autoindex off;
}

location /static-auth {
	internal;
	proxy_pass http://127.0.0.1:8080/v1/auth/static;
	proxy_pass_request_body off;
	proxy_set_header Content-Length "";
	proxy_set_header X-Original-URI $request_uri;
}

On the nonshy website side: have a handler endpoint on /v1/auth/static.

Interesting findings:

  • NGINX will forward regular HTTP cookies along on the subrequest: so if a logged-in user is loading a /static/photos item, their session cookie comes with and we know who they are.
  • We also pass the X-Original-URI, so if we add a "?jwt=" token we can also attach a signature for validation.

Implementation

The JWT token's claims include:

  • Subject (the logged-in username who generated the token)
  • FilenameHash (a 'short' hash of the Filename of a photo: looks like "/a9/c9/a9c9287f-2df8-42c3-8693-25eeb59ee407.jpg")
  • Anyone (boolean - to make a 'non-authenticated' JWT token, e.g. for the chat room)

In the vast majority of on-website use cases (gallery, forums, profiles, etc.) - the signed JWT tokens are for the current user only, are tied to the photo's file name quite well, and expire after 30 seconds.

  • Users can't share links even with other logged-in users, as the logged-in user won't match the JWT token.
  • Users can't cut the "?jwt=" param off one URL and paste it on another, as a hash of the filename is verified in the JWT token.
  • A logged-out browser can't load the image either.

There were a couple of interesting edge cases:

  • The chat room: the nonshy site logs you into the chat via JWT token as well, and includes your avatar URL which others on chat load your picture by. Other users on chat need to be able to load this image, but:
    • Since the chat room is cross-domain, NGINX will not forward a session cookie to the auth function, so the website can't tell who the logged-in user is.
    • The chat room being a separate app to the site, it can't sign a unique URL for each of the other chatters.
  • Profile visibility: if you opt for a "limited logged-out view" so your square profile picture should appear to logged-out users who click your profile, they need to be able to see your square profile picture.

Signatures generated for those images are marked as for "Anyone" and they expire in 7 days rather than 30 seconds (to account for users who lurk in the chat room for days at a time).

Note: if your profile picture is friends-only or private, both the chat room AND your "limited logged-out view" instead show the placeholder yellow/pink avatars, so users with private pics will never have such a long-lived JWT token generated for theirs. Only square cropped avatars are ever given these signatures.

Currently, the actual file names of the .jpg images under /static/photos are not authenticated: if you get the direct link to one, you can always grab it even if not logged in. The site mainly relies on photo filenames to be randomized and unguessable. With NGINX [ngx_http_auth_request](https://nginx.org/en/docs/http/ngx_http_auth_request_module.html) we may be able to protect these files and validate them against a logged-in user. ### How it works On the NGINX side, you configure the /static/ folder to do a subrequest for authentication: ```nginx # Static file access location /static/ { auth_request /static-auth; alias /home/www-data/git/website/web/static/; autoindex off; } location /static-auth { internal; proxy_pass http://127.0.0.1:8080/v1/auth/static; proxy_pass_request_body off; proxy_set_header Content-Length ""; proxy_set_header X-Original-URI $request_uri; } ``` On the nonshy website side: have a handler endpoint on /v1/auth/static. Interesting findings: * NGINX will forward regular HTTP cookies along on the subrequest: so if a logged-in user is loading a /static/photos item, their session cookie comes with and we know who they are. * We also pass the X-Original-URI, so if we add a "?jwt=" token we can also attach a signature for validation. ### Implementation The JWT token's claims include: * Subject (the logged-in username who generated the token) * FilenameHash (a 'short' hash of the Filename of a photo: looks like "/a9/c9/a9c9287f-2df8-42c3-8693-25eeb59ee407.jpg") * Anyone (boolean - to make a 'non-authenticated' JWT token, e.g. for the chat room) In the vast majority of on-website use cases (gallery, forums, profiles, etc.) - the signed JWT tokens are _for_ the current user only, are tied to the photo's file name quite well, and expire after 30 seconds. * Users can't share links even with other logged-in users, as the logged-in user won't match the JWT token. * Users can't cut the "?jwt=" param off one URL and paste it on another, as a hash of the filename is verified in the JWT token. * A logged-out browser can't load the image either. There were a couple of interesting edge cases: * The chat room: the nonshy site logs you into the chat via JWT token as well, and includes your avatar URL which others on chat load your picture by. Other users on chat need to be able to load this image, but: * Since the chat room is cross-domain, NGINX will _not_ forward a session cookie to the auth function, so the website can't tell who the logged-in user is. * The chat room being a separate app to the site, it can't sign a unique URL for each of the other chatters. * Profile visibility: if you opt for a "limited logged-out view" so your square profile picture should appear to logged-out users who click your profile, they need to be able to see your square profile picture. Signatures generated for _those_ images are marked as for "Anyone" and they expire in 7 days rather than 30 seconds (to account for users who lurk in the chat room for days at a time). Note: if your profile picture is friends-only or private, both the chat room AND your "limited logged-out view" instead show the placeholder yellow/pink avatars, so users with private pics will never have such a long-lived JWT token generated for theirs. Only square cropped avatars are ever given these signatures.
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: nonshy/website#51
No description provided.