org: refuse #+INCLUDE and #+SETUPFILE when rendering !111

merged merged by cmc on 2026-08-29 02:00 UTC · krz/gitbay:org-include-sandbox into main

Discussion

cmc

Closes the file-read hole #51 names as its prerequisite.

The hole

renderReadme parsed org with org.New(), whose ReadFile defaults to os.ReadFile. Everything that function handles is content someone pushed — a README, a wiki page, a profile's about text — so a pushed document could read any file the daemon can open.

Demonstrated, not assumed. #+INCLUDE: "<path>" src text rendered the file into the page as a highlighted source block:

#+INCLUDE: "/tmp/secret.txt" src text
→ <pre class="chroma"><code>…SENTINEL-SERVER-SIDE-SECRET…</code></pre>

Two details that make it worse than it first looks:

  • the export html kind injects the file as raw HTML rather than escaped text, so it is not only disclosure
  • an absolute path skips go-org's relative-path join, and a relative one resolves against filepath.Dir(d.Path) — renderReadme passes a bare filename, so that is the daemon's working directory and ../.. traversal reaches anything above it

#+SETUPFILE: reads at parse time by the same route and folds the result into buffer settings.

The fix

orgConfig() refuses both: the file is never opened, and the keyword stays the inert text it already was, so the rest of the document renders unchanged. There is no safe subset to allow instead — the content comes from a git object, so there is no directory to scope a read to.

Also discards go-org's parse warnings, which the default logger wrote to stderr, letting pushed content write to the server's log.

Tests

internal/httpd/orgrender_test.go, five cases: absolute reads across all three include kinds, relative traversal, #+SETUPFILE:, the guard asserted directly on orgConfig().ReadFile, and two that ordinary org still renders.

Verified they actually gate the fix rather than passing for their own reasons — reverting the one-line change makes three of them fail. Full suite green.

Ref #28.