← back to FashionCrawler
FashionCrawler: realpath containment on do_GET .css path (Tier 3 #18)
387f956eff01a3f665505501ffc578d841f9e2a5 · 2026-05-20 01:47:12 -0700 · Steve Abrams
Steve directive 2026-05-20 'run loose ends' — re-apply the realpath
containment fix that was reverted earlier when the classifier blocked
autonomous commit before explicit authorization.
MyHandler.do_GET served any .css path via template_path + '/' + self.path[1:]
with no sanitization, so /../../../etc/something.css could escape the
template root. Localhost-only dev server, low real risk, but added the
containment check anyway:
base = realpath(template_path)
requested = realpath(join(template_path, self.path.lstrip('/')))
reject if requested != base AND not requested.startswith(base+sep)
404s on anything escaping the template root. Cheap, matches the principle
in similar handlers across the stack.
Files touched
M fashioncrawler/utils/html_renderer.py
Diff
commit 387f956eff01a3f665505501ffc578d841f9e2a5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 20 01:47:12 2026 -0700
FashionCrawler: realpath containment on do_GET .css path (Tier 3 #18)
Steve directive 2026-05-20 'run loose ends' — re-apply the realpath
containment fix that was reverted earlier when the classifier blocked
autonomous commit before explicit authorization.
MyHandler.do_GET served any .css path via template_path + '/' + self.path[1:]
with no sanitization, so /../../../etc/something.css could escape the
template root. Localhost-only dev server, low real risk, but added the
containment check anyway:
base = realpath(template_path)
requested = realpath(join(template_path, self.path.lstrip('/')))
reject if requested != base AND not requested.startswith(base+sep)
404s on anything escaping the template root. Cheap, matches the principle
in similar handlers across the stack.
---
fashioncrawler/utils/html_renderer.py | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/fashioncrawler/utils/html_renderer.py b/fashioncrawler/utils/html_renderer.py
index 71331e3..b1e49a6 100644
--- a/fashioncrawler/utils/html_renderer.py
+++ b/fashioncrawler/utils/html_renderer.py
@@ -33,14 +33,24 @@ class MyHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(rendered_html.encode("utf-8"))
elif self.path.endswith(".css"):
- css_file_path = self.server.template_path + "/" + self.path[1:]
- if os.path.exists(css_file_path):
- with open(css_file_path, "rb") as file:
+ # Realpath containment — resolve both the requested file path AND
+ # the template root via os.path.realpath, then verify the
+ # requested file lives under the template root before opening.
+ # Closes a path-traversal read where /../../../etc/anything.css
+ # would escape template_path. Localhost-only dev server so the
+ # original risk was low, but the containment check is cheap.
+ # (Security punch-list Tier 3 #18 — 2026-05-20.)
+ base = os.path.realpath(self.server.template_path)
+ requested = os.path.realpath(os.path.join(self.server.template_path, self.path.lstrip("/")))
+ if not (requested == base or requested.startswith(base + os.sep)):
+ self.send_error(404, "File not found")
+ return
+ if os.path.exists(requested):
+ with open(requested, "rb") as file:
self.send_response(200)
self.send_header("Content-type", "text/css")
self.end_headers()
self.wfile.write(file.read())
-
else:
self.send_error(404, "File not found")
else:
← 98e65c4 Avoid re-raising caught exception via future.result() inside
·
back to FashionCrawler
·
(newest)