Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fasthtml/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
'fasthtml.core._resp': ('api/core.html#_resp', 'fasthtml/core.py'),
'fasthtml.core._route_pn': ('api/core.html#_route_pn', 'fasthtml/core.py'),
'fasthtml.core._send_ws': ('api/core.html#_send_ws', 'fasthtml/core.py'),
'fasthtml.core._static_fpath': ('api/core.html#_static_fpath', 'fasthtml/core.py'),
'fasthtml.core._to_htmx_header': ('api/core.html#_to_htmx_header', 'fasthtml/core.py'),
'fasthtml.core._to_xml': ('api/core.html#_to_xml', 'fasthtml/core.py'),
'fasthtml.core._url_for': ('api/core.html#_url_for', 'fasthtml/core.py'),
Expand Down
11 changes: 9 additions & 2 deletions fasthtml/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,18 +1025,25 @@ def reg_re_param(m, s):
_static_exts = "ico gif jpg jpeg webm css js woff png svg mp4 webp ttf otf eot woff2 txt html map pdf zip tgz gz csv mp3 wav ogg flac aac doc docx xls xlsx ppt pptx epub mobi bmp tiff avi mov wmv mkv xml yaml yml rar 7z tar bz2 htm xhtml apk dmg exe msi swf iso".split()
reg_re_param("static", '|'.join(_static_exts))

def _static_fpath(static_path, relpath):
"Resolved path of `relpath` under `static_path`, or 404 if it escapes it (e.g. `..` traversal)"
base = os.path.realpath(static_path)
fpath = os.path.realpath(os.path.join(base, relpath))
if os.path.commonpath([fpath, base]) != base: raise HTTPException(404)
return fpath

@patch
def static_route_exts(self:FastHTML, prefix='/', static_path='.', exts='static'):
"Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`"
@self.get(f"{prefix}{{fname:path}}.{{ext:{exts}}}")
async def get(fname:str, ext:str): return FileResponse(f'{static_path}/{fname}.{ext}')
async def get(fname:str, ext:str): return FileResponse(_static_fpath(static_path, f'{fname}.{ext}'))

# %% ../nbs/api/00_core.ipynb #b31de65a
@patch
def static_route(self:FastHTML, ext='', prefix='/', static_path='.'):
"Add a static route at URL path `prefix` with files from `static_path` and single `ext` (including the '.')"
@self.get(f"{prefix}{{fname:path}}{ext}")
async def get(fname:str): return FileResponse(f'{static_path}/{fname}{ext}')
async def get(fname:str): return FileResponse(_static_fpath(static_path, f'{fname}{ext}'))

# %% ../nbs/api/00_core.ipynb #f63b7a03
class StaticNoCache(StaticFiles):
Expand Down
39 changes: 37 additions & 2 deletions nbs/api/00_core.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4406,11 +4406,18 @@
"_static_exts = \"ico gif jpg jpeg webm css js woff png svg mp4 webp ttf otf eot woff2 txt html map pdf zip tgz gz csv mp3 wav ogg flac aac doc docx xls xlsx ppt pptx epub mobi bmp tiff avi mov wmv mkv xml yaml yml rar 7z tar bz2 htm xhtml apk dmg exe msi swf iso\".split()\n",
"reg_re_param(\"static\", '|'.join(_static_exts))\n",
"\n",
"def _static_fpath(static_path, relpath):\n",
" \"Resolved path of `relpath` under `static_path`, or 404 if it escapes it (e.g. `..` traversal)\"\n",
" base = os.path.realpath(static_path)\n",
" fpath = os.path.realpath(os.path.join(base, relpath))\n",
" if os.path.commonpath([fpath, base]) != base: raise HTTPException(404)\n",
" return fpath\n",
"\n",
"@patch\n",
"def static_route_exts(self:FastHTML, prefix='/', static_path='.', exts='static'):\n",
" \"Add a static route at URL path `prefix` with files from `static_path` and `exts` defined by `reg_re_param()`\"\n",
" @self.get(f\"{prefix}{{fname:path}}.{{ext:{exts}}}\")\n",
" async def get(fname:str, ext:str): return FileResponse(f'{static_path}/{fname}.{ext}')"
" async def get(fname:str, ext:str): return FileResponse(_static_fpath(static_path, f'{fname}.{ext}'))"
]
},
{
Expand Down Expand Up @@ -4451,7 +4458,7 @@
"def static_route(self:FastHTML, ext='', prefix='/', static_path='.'):\n",
" \"Add a static route at URL path `prefix` with files from `static_path` and single `ext` (including the '.')\"\n",
" @self.get(f\"{prefix}{{fname:path}}{ext}\")\n",
" async def get(fname:str): return FileResponse(f'{static_path}/{fname}{ext}')"
" async def get(fname:str): return FileResponse(_static_fpath(static_path, f'{fname}{ext}'))"
]
},
{
Expand All @@ -4465,6 +4472,34 @@
"assert 'THIS FILE WAS AUTOGENERATED' in cli.get('/README.md').text"
]
},
{
"cell_type": "markdown",
"id": "c1a7f3d2",
"metadata": {},
"source": [
"`..` segments are resolved before the file is served, so a request can only reach files under `static_path`; anything that escapes it is a 404, even when the file exists."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c2b8e4a1",
"metadata": {},
"outputs": [],
"source": [
"travd = Path(tempfile.mkdtemp())\n",
"(travd/'public'/'sub').mkdir(parents=True)\n",
"(travd/'public'/'sub'/'ok.md').write_text('ok')\n",
"(travd/'public'/'secret.md').write_text('SECRET')\n",
"(travd/'secret.md').write_text('SECRET')\n",
"trav_app = FastHTML()\n",
"trav_app.static_route('.md', static_path=travd/'public'/'sub')\n",
"trav_cli = TestClient(trav_app)\n",
"test_eq(trav_cli.get('/ok.md').text, 'ok')\n",
"test_eq(trav_cli.get('/%2e%2e/secret.md').status_code, 404)\n",
"test_eq(trav_cli.get('/%2e%2e/%2e%2e/secret.md').status_code, 404)"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down