What to know about python static file security when using StayPresent’s web.html/markdown — directory exposure, path traversal, and URL filtering.
Security Notes for Serving Static Files with StayPresent
Serving a status dashboard or a rendered README with web.html()/web.markdown() is convenient precisely because it automatically picks up neighboring CSS, JS, and images with no extra configuration. That same convenience has a security dimension worth understanding clearly before you point it at a directory. This covers python static file security as it applies specifically to StayPresent: what’s protected automatically, and what’s still your responsibility to manage.
Table of Contents
- The Directory-Wide Exposure Behavior
- Why This Is Intentional
- Path Traversal Protection
- The One-Time Directory Warning
- Markdown-Specific Protections: Escaping
- Markdown-Specific Protections: URL Scheme Filtering
- What’s Rejected vs What’s Allowed
- Structuring Directories Safely
- Full Example
- Best Practices
- Common Mistakes
- FAQs
- Conclusion
The Directory-Wide Exposure Behavior
When you call web.html("templates/index.html") or web.markdown("docs/guide.md"), StayPresent doesn’t just serve that one file — it serves every file in that file’s directory, not only the specific CSS/JS/image files actually referenced from the page. This is what makes relative asset links (href="style.css", src="images/logo.png") work automatically without any extra configuration on your part.
The consequence: if a .env file, your bot’s own source code, or a .git/ directory happens to sit in that same directory, it becomes downloadable by anyone who requests it by name — whether or not anything on the page actually links to it.
templates/
├── index.html
├── style.css <- intentionally public, referenced from index.html
├── .env <- NOT referenced anywhere, but still reachable
Enter fullscreen mode Exit fullscreen mode
Why This Is Intentional
This isn’t an oversight — it’s what makes web.html()/web.markdown() usable with zero configuration for the overwhelming majority of legitimate cases (a template directory containing only the page and its assets). Requiring an explicit allowlist of every servable file for every deployment would defeat the “read fresh from disk” simplicity that makes these two functions useful in the first place. StayPresent’s own documentation notes that an opt-in allowlist restricting exposure to only referenced files is being considered for a future release — but as things stand, the responsibility for directory contents is on you.
Path Traversal Protection
What StayPresent does protect against automatically is path traversal — a request trying to escape the target directory entirely, via ../ sequences, absolute paths, or similar tricks. Static asset lookups use send_from_directory internally, which refuses to serve any path that would resolve outside the intended directory. So while every file inside templates/ is reachable, nothing outside it is, regardless of how a request tries to reference it.
The One-Time Directory Warning
StayPresent logs a one-time WARNING-level message through the "staypresent" logger the first time html() or markdown() exposes a given directory as a static-asset fallback — specifically as a reminder that this exposure is directory-wide:
import logging
logging.getLogger("staypresent").setLevel(logging.WARNING)
# ... elsewhere ...
staypresent.web.html("templates/index.html")
# WARNING logged once: directory 'templates/' is now servable as static assets
Enter fullscreen mode Exit fullscreen mode
This warning fires once per directory per process, not once per request — it’s meant to catch your attention during development or the first deploy, not spam your logs continuously.
Markdown-Specific Protections: Escaping
Beyond directory exposure, web.markdown() has its own layer of protection specific to rendering user-authored (or at least file-authored) content into HTML. Plain text — and every recognized Markdown construct — is HTML-escaped before rendering, applied exactly once per character, including inside link/image URLs and titles. A .md file containing literal <script> tags, or stray <, >, & characters, cannot inject markup into the rendered page. Only a fixed list of recognized block-level raw-HTML tags (the kind used for centered logo/badge headers) is ever passed through unescaped — arbitrary inline HTML is not.
Markdown-Specific Protections: URL Scheme Filtering
Escaping alone doesn’t stop an executable destination — a link that’s syntactically valid Markdown but points somewhere dangerous. web.markdown() checks link/image URLs against a scheme blocklist independently of escaping:
-
javascript:andvbscript:— rejected for both links and images, since these run arbitrary script directly. -
file:— rejected for both, since it enables local filesystem access. -
data:— rejected for links specifically (it can smuggle a full HTML document, including script, into a single click), but still allowed for images, since inlinedata:images are a common and inert pattern for embedding small icons.
A rejected URL falls back to plain, already-escaped text:
[click me](javascript:alert(1))
Enter fullscreen mode Exit fullscreen mode
renders as the plain text click me — not a working link.
What’s Rejected vs What’s Allowed
URL type Links Imageshttp(s)://
Allowed
Allowed
Relative (/path, ./file)
Allowed
Allowed
Anchor (#section)
Allowed
Allowed
data:
Rejected
Allowed
javascript: / vbscript:
Rejected
Rejected
file:
Rejected
Rejected
Structuring Directories Safely
The practical takeaway is directory hygiene: keep the directory passed to web.html()/web.markdown() limited to files you’re genuinely comfortable being publicly reachable.
# Safer structure
public/
├── dashboard.html
├── style.css
├── logo.png
secrets/
├── .env
├── credentials.json
Enter fullscreen mode Exit fullscreen mode
As long as secrets/ isn’t the directory (or a parent of the directory) passed to web.html()/web.markdown(), path traversal protection keeps it unreachable regardless of what’s inside public/.
Full Example
import logging
import staypresent
logging.getLogger("staypresent").setLevel(logging.WARNING)
# 'public/' contains only dashboard.html, style.css, and logo.png —
# nothing sensitive lives alongside it.
staypresent.web.html("public/dashboard.html")
staypresent.run("bot.py")
Enter fullscreen mode Exit fullscreen mode
Best Practices
- Treat any directory passed to
web.html()/web.markdown()as fully public — audit its contents the same way you’d audit a directory served by a real static file server. - Keep secrets,
.envfiles, and source code in a separate directory tree entirely, never alongside a served template or Markdown file. - Leave
"staypresent"logging atWARNINGor above in production specifically so the one-time directory-exposure notice doesn’t get lost inINFO-level noise.
Common Mistakes
- Putting a template file directly in a project’s root directory, which then exposes the entire project — source files, config, everything — as static assets.
- Assuming Markdown rendering sanitizes destinations the same way it escapes text. Escaping and scheme filtering are two separate protections — both matter, and both are handled, but it’s worth understanding they’re not the same mechanism.
- Relying on “nothing links to it” as a security boundary. Every file in the directory is reachable by direct request, whether or not the rendered page itself contains a link to it.
FAQs
Does this affect web.text() or web.json()?
No — directory-wide static asset exposure only applies to web.html() and web.markdown(), since only those two serve files from disk in the first place.
Can I disable static asset serving entirely?
Not currently — it’s inherent to how html()/markdown() work. The mitigation is directory hygiene, not a configuration flag.
Does the URL scheme blocklist apply to web.html() too?
No — that specific protection is part of the Markdown renderer. web.html() serves whatever HTML you’ve written as-is, so any sanitization of links/scripts inside a raw .html file is your own responsibility.
Conclusion
Python static file security with StayPresent comes down to two layers: what’s protected automatically (path traversal, HTML escaping, dangerous URL schemes in rendered Markdown) and what’s still on you (directory contents). Understanding the difference — and keeping served directories limited to genuinely public files — is what keeps a convenient zero-config feature from becoming an accidental information leak.
pip install staypresent[prod]
Enter fullscreen mode Exit fullscreen mode
답글 남기기