Hello @nebulon - I'm using Surfer as a simple "link-only" static host. Access is Public, Public folder listing is off, and each set of pages lives under an unguessable folder name, so only people who get the link can read it. That model holds for normal browsing: with listing off, unknown folders return 404. But /api/zip gets around it.
Environment
Cloudron 10.0.5
Surfer package 7.2.6 (upstream at c5329dc, 2026-10-06)
Settings: access = Public, Public folder listing = off
Steps to reproduce
Set Surfer access to Public and turn Public folder listing off.
Upload some content into a folder, for example /some-unguessable-name/index.html.
From a logged-out browser or curl:curl -o all.zip 'https://surfer.example.com/api/zip?paths=%5B%22%2F%22%5D'
unzip -l all.zip
Expected: the request is refused (401/404), because folder listing is off, so anonymous visitors shouldn't be able to enumerate or bulk-download content they don't have a URL for.
Actual: 200 application/zip containing the entire site root, including every folder name, so anyone who can reach the host can discover and download everything.
Why it happens
src/routes/index.js:79: router.get('/api/zip', access.handleProtection, publicSite.zipDownload);. On a Public site, handleProtection lets everyone through, and folderListingEnabled is never consulted.
src/routes/public.js zipDownload() accepts any paths inside the site root, and "/" is allowed.
Related: the deployment query parameter (sites.rootForQuery()) lets an anonymous caller zip another named site's root (public-<name>) through the same endpoint, from any of the app's domains.
The only callers I could find are the public listing UI (frontend/utils.js:55, used by Public.vue) and the admin file view (views/FilesView.vue). So when listing is off, the anonymous use case for this endpoint doesn't exist.
Suggested fix: a "Public zip downloads" on/off setting
Add a setting next to "Public folder listing", stored the same way as folderListingEnabled:
Public zip downloads: when enabled, anonymous visitors can download folders as a .zip. When disabled, only signed-in users can (the admin file view keeps working).
Default: existing installs inherit their current folderListingEnabled value, so nobody's behavior changes on update. Listing-on sites keep their download button, and listing-off sites stop leaking.
Gate: when the switch is off, /api/zip requires auth. A deployment= request always requires auth, because anonymous visitors shouldn't be able to zip other sites.
Public UI: hide the folder/multi-select "Download" action when the switch is off. Single-file downloads use ?download and keep working.
Rough sketch (untested):
// src/settings.js
const DEFAULTS = { folderListingEnabled: false, publicZipEnabled: null, /* ... */ };
// normalize(): null/undefined -> follow folderListingEnabled (backwards compatible)
publicZipEnabled: values.publicZipEnabled == null
? (values.folderListingEnabled === true || values.folderListingEnabled === 'true')
: (values.publicZipEnabled === true || values.publicZipEnabled === 'true'),
// storedValue(): add 'publicZipEnabled' to the boolean keys
// src/routes/settings.js
// get(): publicZipEnabled: !!config.publicZipEnabled,
// put(): optional, so older clients that don't send it keep working
if ('publicZipEnabled' in req.body && typeof req.body.publicZipEnabled !== 'boolean') return next(new HttpError(400, 'publicZipEnabled must be a boolean'));
if ('publicZipEnabled' in req.body) config.publicZipEnabled = req.body.publicZipEnabled;
// src/routes/index.js
function zipAccess(req, res, next) {
if (config.publicZipEnabled && !req.query.deployment) return access.handleProtection(req, res, next);
return auth.requireAuth(req, res, next); // admin sessions / app passwords still work
}
router.get('/api/zip', zipAccess, publicSite.zipDownload);
// frontend/views/SettingsView.vue: one more <Switch v-model="settings.publicZipEnabled">, like folder listing
// frontend/Public.vue: hide the zip-download action when !settings.publicZipEnabled
If a setting is more than you'd like to add, the minimal version is simply to tie the gate to config.folderListingEnabled instead of a new key.