Skip to content

Limit the public image, upload and answer endpoints - #212

Merged
PhilReinking merged 9 commits into
mainfrom
fix/bits-156-limit-public-endpoints
Oct 10, 2026
Merged

PhilReinking merged 9 commits into
mainfrom
fix/bits-156-limit-public-endpoints

Conversation

@PhilReinking

@PhilReinking PhilReinking commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

The public image, upload and answer endpoints accepted any image parameters, any upload and answers of any size. This limits them to what the app uses and to each question's settings.

Fixes BITS-156

  • Images: only form images, only the 5 variants the app requests, 120 requests/min per IP.
  • Uploads: type, size (max 8 MB) and count from the question's file settings, 30/min per IP.
  • nginx and post_max_size allow 9 MB, so an 8 MB file reaches PHP and its upload checks.
  • Answers max 30 KB each, session params max 8 KB as JSON. The editor's max file size is 8 MB, not 16.

Heads-up: an upload outside the question's settings, an answer over 30 KB or an overlong form link now fails with 422, and respondents see the retry message. Questions saved with more than 8 MB keep the value, but uploads stop at 8 MB. Behind a proxy, TRUSTED_PROXIES must list it, or all visitors share one limit.

Tested: full PHP suite. Each new limit test fails without its fix. Both configs load in the base image.
Not tested: by hand in a browser, a real 8 MB upload through nginx.

Details for review
  • Image variants: none (logo), w=256&q=75 (editor preview), w=1600|1920|2880&fm=webp (form background). Only form.blade.php, ImageUpload.vue and the plain logo URL build image URLs. No mail, embed script, meta tag or doc does. Anything else is a 404 and renders nothing.
  • Uploads need a file question with a submitted answer. The limits come from the block's file interaction, like on the form page, also when the block still holds an older non-file interaction. The type is read from the file content. Count is at most 10 when unset.
  • Retry: the form page skips files that already went through, so earlier uploads stay and still count.
  • 30 KB per answer: answers are stored encrypted in a 64 KB column, and encryption almost doubles the size.

🤖 Generated with Claude Code

PhilReinking and others added 3 commits October 10, 2026 12:25
- Serve only form images, in the variants the app requests, and throttle the image route.
- Check uploads against the file question's allowed types, size and count, require a file, and throttle the upload route.
- Cap answer and session parameter size.
- A submit that announces uploads replaces the earlier files of those questions, so a retry fits the count.
- The editor's max file size matches the server's 8 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changes at 55272bc, CI green. Two musts: a retry after a failed upload deletes files that went through, and the changelog line.

Checked beyond the PR text, on a local server and the pr-212 image: logo, background and editor preview load, also on a duplicated form; other image variants 404 and store nothing; refused type, missing answer, no file and a 4th file get 422; the editor saves 8 for 20; 7.9 MB uploads, 8.0 MB gets nginx's 413.

Comment on lines +53 to +58

// the form page uploads all files again after this call, so a retry must not count the earlier ones
if ($request->boolean('is_uploading')) {
// strings, so MySQL can't match a numeric key against every uuid
$session->deleteUploads(array_map('strval', array_keys($request->input('payload'))));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

On a retry the form page skips files that already went through (91f921e), so this deletes them for good. Tried it: small.pdf uploaded, big.pdf refused, retry: the answer lists small.pdf, no file left. Drop the cleanup and deleteUploads, or make the form re-send every file on a retry.

Suggested change
// the form page uploads all files again after this call, so a retry must not count the earlier ones
if ($request->boolean('is_uploading')) {
// strings, so MySQL can't match a numeric key against every uuid
$session->deleteUploads(array_map('strval', array_keys($request->input('payload'))));
}

Comment thread CHANGELOG.md Outdated
- **Upgrade step:** form links must start with http(s):// or mailto:. Saved links that don't (like a bare social handle or a www. address) are hidden on the public form: enter them again as full URLs. Templates with such links don't import. Form texts are cleaned before they show. (#193)
- Webhook responses show as plain text in the submissions view. (#196)
- Question labels in the logic editor's pickers show as plain text. (#199)
- Public forms have limits: images load only in the sizes the form uses, uploads must match the question's file types, size and count (at most 8 MB per file, also in the editor), answers can be up to 30 KB and form link parameters up to 8 KB. Images and uploads are rate-limited per visitor, so behind a proxy check that `TRUSTED_PROXIES` lists it. (#212)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Say plainly that the image address now only serves form logos and backgrounds:

Suggested change
- Public forms have limits: images load only in the sizes the form uses, uploads must match the question's file types, size and count (at most 8 MB per file, also in the editor), answers can be up to 30 KB and form link parameters up to 8 KB. Images and uploads are rate-limited per visitor, so behind a proxy check that `TRUSTED_PROXIES` lists it. (#212)
- Public forms have limits: the image address only serves form logos and backgrounds, in the sizes the form uses. Uploads must match the question's file types, size and count (at most 8 MB per file, also in the editor), answers can be up to 30 KB and form link parameters up to 8 KB. Images and uploads are rate-limited per visitor, so behind a proxy check that `TRUSTED_PROXIES` lists it. (#212)

{
$request->validate([
// params come from the form link, and nginx stops URLs at 8 KB
'params' => ['array', 'nullable', new MaxSize(8192)],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Serialized, each param adds about 15 bytes, so a 4 KB link with 500 short params is refused. The form page then doesn't load at all, there's no retry message. Params are stored as JSON, so measure the JSON length here: that's close to the link's length.

PhilReinking and others added 5 commits October 10, 2026 12:46
The form page skips files that already went through on a retry, so the cleanup deleted them for good. The upload count check still holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Serialized, each param adds about 15 bytes, so a long form link with many short params was refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nginx and post_max_size refused the request of an 8 MB file, since the request is a bit larger than its file. Now PHP's upload limit and the upload checks decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK at 1b8792d, CI green. All points from the last review are fixed.

Checked beyond the PR text, on a local server: small.pdf uploads, big.pdf is refused, the retry and the submit without big.pdf both keep small.pdf, and the form ends on "Form Submitted". On the pr-212 image, 8.0 MB uploads and 8.1 MB gets a 422. A link with 500 short params now opens a session, and one 8.3 KB param gets a 422.

…blic-endpoints

# Conflicts:
#	routes/web.php

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK at c47e1fe, CI green. The merge of main only resolves routes/web.php: the image route keeps its throttle and the removed preview route stays gone. Compared with 1b8792d, the PR's own files are unchanged, and the CHANGELOG keeps main's lines plus this PR's one line.

@PhilReinking
PhilReinking merged commit 9518df3 into main Oct 10, 2026
4 checks passed
@PhilReinking
PhilReinking deleted the fix/bits-156-limit-public-endpoints branch October 10, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant