starlette: StaticFiles middleware doesn't follow symlinks
Checklist
- The bug is reproducible against the latest release and/or
master. - There are no similar issues or pull requests to fix it yet.
Describe the bug
The StaticFiles middleware is checking the os.realpath of a file and returning a 404 for symlinks that lead outside the static directory.
To reproduce
- create a minimal app with a staticfiles middleware
- put a symlink in your static directory. the link’s target must be above the static directory.
- you’ll get a 404
Expected behavior
Support symlinks in static directory.
The use case for symlinks in static is to target frontend assets that are being generated in file-watch mode.
Actual behavior
Debugging material
It’s happening here: https://github.com/encode/starlette/blob/b95acea973c20eea3e7cbbca42d09b1f5d4a3412/starlette/staticfiles.py#L147-L149
Environment
- OS: linux
- Python version: 3.7.5
- Starlette version: 0.13.8
Additional context
I’m happy to post a PR for this if useful, ideally adding a bool param to the StaticFiles middleware that allows symlinks.
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 6
- Comments: 21 (10 by maintainers)
Report about the incident: https://github.com/encode/starlette/pull/1681#issuecomment-1152178256
Yeah, I’m going to clarify things in a few hours. 👍
I would be interested to know why the change had to be reverted.
A fix is already merged ,and it will be available on the next release (we are at 0.20.1 by the time I wrote this). 🙌
Currently using this workaround until a new version of Starlette is released. It’s really hacky but it gets the job done 🤣
dagit_wrapper.py
flask
In flask, static files are served with:
send_static_file -> send_from_directory -> werkzeug.safe_join
safe_joinchecks that the provided path isn’t above the current directory, but it will happily follow a symlink that points above the process’s working directory (I tested this).django
In django, the staticfiles finders call FileSystemStorage.path which uses a similar safe_join
from testing, it seems like this will also follow a symlink above the working directory
edit note django by default only serves static files when
DEBUG=1, see here for examplesymlink upload
Re someone’s point about what if users can write symlinks to the StaticFiles dir – would be interested to know if this is possible via normal HTTP file uploads.
In ext4, for example, I think symlink status is stored in the inode table along with the uga/rwx mode bits. From skimming the nginx upload module, it doesn’t look like those get transferred. It sets permission here from
store_access, which is a config, not from the upload. (I could be wrong about this).Also the RFC for HTTP uploads doesn’t talk about symlinks or mode bits https://www.rfc-editor.org/rfc/rfc1867.
I’m assuming starlette also doesn’t receive or set mode bits for file uploads?
opt-in
I like the idea of an opt-in
follow_symlinksetting – in my case I would enable it in DEBUG and disable in prod.sorry for not thinking through security issues when posting the feature request
great catch, thanks to kind stranger on gitter, and thanks for being quick + transparent about the fix
here’s a subclass that solves the problem
you install it the same way you install the stock StaticFiles. I used it with starlette==0.13.8, not sure if it works on newer versions.
update jan 2022: this is broken with starlette 0.17. You can patch by:
async def->defaio_stat->os.statupdate jun 2022: this patch seems to not be vulnerable to the same security issue as #1681 – a request for
/static/../../../etc/passwdseems to be getting rewritten to/(tested with starlette 0.17.1, fastapi 0.73.0). I’m guessing the..are being processed before the request hits the middleware.This is just luck, I wasn’t thinking defensively either. I would recommend disabling my patch in production. (in my case I only need it in local dev mode for hitting incremental webpack builds).
post the PR!!!