Private files behind nginx: with POSTULO_MEDIA_ACCEL_PREFIX the file policy and nosniff never reach the browser #415
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#415
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the private-file delivery code against the wiki's proxy guidance.Checked again on 2026-10-01 against
mainat0a5cad587: still present.core/files.pyand both test files are unchanged. In the wiki, since moved on to9aefad9, the setting's entry is now atConfiguration.md:258and still shows no headers;Hardening.md:54-56is as it was.serve_private_fileputsContent-Security-Policy: default-src 'none'; sandbox(FILE_POLICY, #264) andX-Content-Type-Options: nosniffon every private file. WhenPOSTULO_MEDIA_ACCEL_PREFIXis set, those headers sit on an empty Django response that nginx consumes for itsX-Accel-Redirect. On that internal redirect nginx carries over only a fixed set of upstream headers (Content-Type,Content-Disposition,Cache-Control,Expires,Accept-Ranges,Set-Cookie). Anything else is dropped unless the internal location adds it again, and nothing in the wiki says to. With this option on, the second of the two SVG defences is gone, and no test notices.core/files.py:98-117(serve_private_file: the accel branch builds the response that later receivesX-Content-Type-OptionsandContent-Security-Policy);core/files.py:51-62(FILE_POLICY, described as "the half that holds if the sanitiser is ever wrong"); wikiConfiguration.md:256andHardening.md:54-56(the setting and "mark the locationinternal", with no headers);tests/test_private_media.py:132-137(test_nginx_serves_the_bytes_when_configuredchecks onlyX-Accel-Redirectand the empty body);tests/security/test_svg.py:7-9(promises two defences).POSTULO_MEDIA_ACCEL_PREFIXand adds aninternallocation. A company logo stored as SVG (jobs:company_logo, served inline) then arrives with no sandbox policy and nonosniff. Visited directly, it is a same-origin document, and onlycore.pictures.sanitise_svgstands between it and the origin. Logos are often fetched from a company's own website, so their content is chosen by a third party. Other inline files (avatars, sent CVs, picture renderings of captures) lose the same headers. A PDF rendering of a capture is not exposed this way, because it is served as an attachment.POSTULO_MEDIA_ACCEL_PREFIXentry and in Hardening, withadd_header Content-Security-Policy "default-src 'none'; sandbox" always;andadd_header X-Content-Type-Options nosniff always;. Say incore/files.py's module docstring that the proxy must add these headers in this mode. As a code-side backstop, serveimage/svg+xmlthrough Django even when acceleration is configured, so the one type the policy exists for never depends on the proxy. Intests/security/test_svg.py's docstring, say which defence depends on the proxy.Done when
tests/test_private_media.pyhas a test that setsPOSTULO_MEDIA_ACCEL_PREFIX, serves an SVG and asserts that the response is not anX-Accel-Redirectand carriesFILE_POLICY. It fails today.POSTULO_MEDIA_ACCEL_PREFIXshows theinternallocation with bothadd_headerlines.