Skip to content

upload/image: Require login and escape markup in the JSON answer - #511

Open
guidol-ive wants to merge 1 commit into
PlanBCode:mainfrom
guidol-ive:image-upload-login
Open

guidol-ive wants to merge 1 commit into
PlanBCode:mainfrom
guidol-ive:image-upload-login

Conversation

@guidol-ive

Copy link
Copy Markdown
Collaborator

Fixes #416: the image upload endpoint accepted uploads without login and reflected the file name unescaped in a text/html response.

Change

In system/core/pages.php, upload/image:

  • Login: unless the visitor is logged in, the answer is HTTP 403 with [{"error": ...}], the format the editor already shows as an alert, and the existing "login-to-perform-action" message. This happens before any file handling, so nothing is stored.
  • Escaping: the JSON answer is encoded with JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT, so <, >, &, ' and " are sent as \uXXXX. The decoded values are unchanged, so the editor still gets the original file name for the alt text.

The content type stays text/html on purpose: the WYMeditor plugins post the upload into a hidden iframe and parse body.html() of that iframe. With application/json, some browsers show their JSON viewer in the iframe and the upload breaks.

The endpoint still has no CSRF token check; with this change only members reach it, and the session cookie is SameSite=Strict. A token in the editor's upload form could be a follow-up.

Testing

Tested in a php:<version>-apache container on PHP 8.5, and on PHP 8.2 with #475 (now merged into main), against a copy of a ~1,330-page data set. The uploads were a 1×1 PNG, posted as multipart/form-data like the editor does.

Check main this PR
anonymous upload stored (200), file added to data/images refused (403), no file added
logged-in upload stored, image served stored, image served
logged-in upload named photo <b>bold.png raw <b> in the response sent as <b>; decodes to the same name
anonymous request of all 1,333 pages — no failures
logged in, every page and its edit view — only the 82 failures already reported in #406

The results are the same on 8.2 and 8.5. No new PHP warnings. php -l passes on both.

A side note from testing: PHP keeps only the part after the last / of an uploaded file name, so a name with a closing tag such as </b> arrives cut off. That is PHP's own behaviour and not changed here.

Note: this patch is a suggestion and a starting point, not a finished fix. Please review it and adapt it as you see fit.

🤖 Generated with Claude Code

The image upload endpoint accepted uploads from anonymous visitors and
echoed the client-side file name in a text/html response without
escaping. Refuse the upload with HTTP 403 unless the visitor is logged
in, and encode the JSON with the JSON_HEX_* flags so no markup
characters appear in the response body. The content type stays
text/html, since the editor reads the answer from a hidden iframe.

Fixes PlanBCode#416

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@guidol-ive guidol-ive added Type: Bug Level: Easy Under 4 hours of work Value: Security Impact: High Many installs or users hit hard: core use blocked, data loss or acute security risk. Status: Needs review labels Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Impact: High Many installs or users hit hard: core use blocked, data loss or acute security risk. Level: Easy Under 4 hours of work Status: Needs review Type: Bug Value: Security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: image upload endpoint accepts uploads without login and reflects the file name in an HTML response

1 participant