Security

Architecture — part 13 of 13

The threat model is a static site with no server of its own: nothing here listens on a port, so what remains to get wrong is what the pages carry, what the working directory holds, and the one thing that arrives from outside -- a post sent over the SSH the machine already has.

  • Content-Security-Policy, delivered as a meta tag, because a static host may not let you set headers. Fonts are self-hosted, so no third-party origin is needed for them. A media player's origin is admitted only on the pages that actually embed one -- the build knows which posts carry a provider, so an archive page that embeds nothing allows nothing; YouTube's two origins are the exception and are allowed everywhere. The one inline script is allowlisted by content hash, see The client side.
  • Draft URLs carry a SecureRandom token and noindex. That makes a preview link unguessable and keeps it out of search results. It is not access control: anyone the link reaches can read the draft, which is the point of being able to send one.
  • Escaping is consistent for everything that came off a network -- Fediverse display names, avatar URLs, reply text -- in both the HTML the build writes and the DOM the client builds. The one exception is Mastodon's own sanitized status HTML, see The client side.
  • Input that came off a wire enters at one place, scripts/receive.sh, which listens on nothing: it runs as the forced command of an SSH key. What a stranger chooses is a filename and a stream of bytes, so a name may carry no path, no leading dot and no control character, the read is bounded by BLOGSH_MAX_MB before anything lands, and the markdown is parsed confined -- a picture may be named only by a bare filename, because ![](/etc/passwd) would otherwise be read into the post and published. There is no archive format anywhere in it, which is the whole reason there is nothing to unpack.
  • env.sh holds the live credentials, stays out of git, and is mode 600. The wizards leave a .bak of whatever they rewrote at the same mode, so it still holds the previous tokens -- remove it once one has been rotated.
  • No third-party tracking goes into post data, and the sidebar widgets are fetched server-side by cron rather than by the visitor's browser, so reading a page contacts one origin.