Skip to content

fix(server): set no-cache headers on private project export downloads (SEC-04) - #2360

Open
wilfredmulenga wants to merge 5 commits into
mainfrom
fix/sec-04-export-cache-control
Open

fix(server): set no-cache headers on private project export downloads (SEC-04)#2360
wilfredmulenga wants to merge 5 commits into
mainfrom
fix/sec-04-export-cache-control

Conversation

@wilfredmulenga

Copy link
Copy Markdown
Contributor

Summary

GET /export/:filename streamed a project's export zip with no Cache-Control header, relying entirely on intermediary defaults. The URL is a predictable function of the project ID, so a CDN or shared cache placed in front of this service could store an authorized response and later serve it to a different, unauthenticated requester of the same URL.

Checked the deployed infrastructure: no CDN or shared cache actually sits in front of this service today (enable_cdn is false everywhere it matters), so this isn't an active leak right now. This closes the gap regardless, since it's a real bug independent of the current network setup and cheap to fix.

Fix

Apply the same privateCache middleware already used on other sensitive routes (e.g. /api/graphql), so the response always carries Cache-Control: private, no-store, no-cache, must-revalidate, regardless of what sits in front of the server.

Test plan

  • go build ./... passes
  • Verified live against the local dev server (curl showed the header present with the fix, absent when reverted)
  • Added a regression test asserting the header is always set, even on a rejected request
  • Verified the regression test fails when the fix is reverted, confirming it catches the regression

GET /export/:filename streamed a project's export zip with no
Cache-Control header, relying entirely on intermediary defaults. The
URL is a predictable function of the project ID, so a CDN or shared
cache placed in front of this service could store an authorized
response and later serve it to a different, unauthenticated
requester of the same URL.

Apply the same privateCache middleware already used on other
sensitive routes, so the response is never cached regardless of what
sits in front of the server.
Covers SEC-04: asserts /export/:filename always sets a no-cache
Cache-Control header, even on a rejected request, and verified this
fails without the privateCache middleware in place.
Copilot AI lite review requested due to automatic review settings August 5, 2026 07:38
@github-actions github-actions Bot added the server label Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a potential caching/security gap on the export download endpoint by ensuring export ZIP responses are not cacheable by intermediaries, matching the behavior already used on other sensitive routes.

Changes:

  • Adds the existing privateCache middleware to GET /export/:filename to emit Cache-Control: private, no-store, no-cache, must-revalidate.
  • Adds a regression test to assert the Cache-Control header is set on a rejected request.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
server/internal/app/export.go Adds privateCache middleware to the export download route.
server/internal/app/export_test.go Introduces a regression test asserting Cache-Control is set on rejected export requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/internal/app/export.go Outdated
Echo composes route middleware so the last one in the list runs
closest to the handler. privateCache was listed last, so if
optionalAuth short-circuited (e.g. a bad Authorization header
triggering a 401 before calling next), the response went out with no
Cache-Control header, missing the response class this fix was meant
to cover.

Move privateCache first so it's outermost and always runs, setting
the header on every response regardless of where the chain stops.
@wilfredmulenga wilfredmulenga changed the title fix(server): set no-cache headers on private project export downloads fix(server): set no-cache headers on private project export downloads (SEC-04) Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants