Security Notes for Serving Static Files with StayPresent

작성자

카테고리:

← 피드로
DEV Community · John Wick · 2026-07-30 개발(SW)

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

  1. The Directory-Wide Exposure Behavior
  2. Why This Is Intentional
  3. Path Traversal Protection
  4. The One-Time Directory Warning
  5. Markdown-Specific Protections: Escaping
  6. Markdown-Specific Protections: URL Scheme Filtering
  7. What’s Rejected vs What’s Allowed
  8. Structuring Directories Safely
  9. Full Example
  10. Best Practices
  11. Common Mistakes
  12. FAQs
  13. 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: and vbscript: — 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 inline data: 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 Images http(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, .env files, and source code in a separate directory tree entirely, never alongside a served template or Markdown file.
  • Leave "staypresent" logging at WARNING or above in production specifically so the one-time directory-exposure notice doesn’t get lost in INFO-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

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다