Skip to content

Download pasted RTD images in the browser instead of on the server - #227

Open
unilynx wants to merge 2 commits into
mainfrom
feature/rtd-client-side-image-paste
Open

unilynx wants to merge 2 commits into
mainfrom
feature/rtd-client-side-image-paste

Conversation

@unilynx

@unilynx unilynx commented Nov 12, 2026

Copy link
Copy Markdown
Member

Why

When content with images is pasted into an RTD, the editor asked the server to download each image URL through getImgFromRemoteURL on the publisher form service. That service is reachable without authentication, so anyone could make the server fetch arbitrary URLs (SSRF), with only weak port and credential checks in place.

What changes

  • Pasted images are downloaded by the browser and uploaded through the existing getUploadedFileFinalURL, the same path the image upload button uses. RPC_GetImgFromRemoteURL and its TypeScript interface entry are removed.
  • data: URLs are decoded directly instead of fetched, because CSP connect-src (eg. in the backend) blocks fetch() of data:/blob:.
  • 'Copy image' pastes use the clipboard's image file when the clipboard offers exactly one <img> and one image file. This avoids needing CORS on the remote URL.
  • Validation: png, jpeg, gif, webp and avif only, max 20 MB, 30 s timeout, and the image must decode (createImageBitmap). Fetches use credentials: "omit" and referrerPolicy: "strict-origin".
  • Upload placeholders keep the pasted image's dimensions from width/height attributes or pixel style dimensions. The spinner isn't stretched (object-fit: none).
  • getUploadedFileFinalURL treats webp/avif as images (image cache URL instead of a file URL).
  • Server-side fix in ValidateAndCleanupImage: non-image data: URLs were disposed but then still stored, and invalid data: URLs weren't handled. Both are now discarded.

Known limitations

  • Remote images whose server doesn't send CORS headers can no longer be imported and are dropped, as failed downloads were before. A TODO marks where the editor should tell the user how many images couldn't be copied.
  • Cross-origin downloads in the backend are blocked by the portal's CSP. It sets connect-src 'self' https://*.googleapis.com, so cross-origin https: image URLs in pasted HTML will be dropped there unless connect-src is widened. Same-origin images, data: URLs and clipboard image files work.

Tests

  • New steps in tollium.richdoc.teststructured: unusable pasted images are rejected (non-image data: URL, corrupt png, 404, file: URL), and png and webp clipboard files are imported when the HTML's remote URL can't be fetched.
  • Passing: tollium.richdoc.teststructured, tollium.richdoc.testfree, tollium.richdoc.testrteservice, wh.jscomponents.rte.teststructured, wh.jscomponents.rte.teststructured-copypaste, wh.jscomponents.rte.testbase, publisher.forms.testrte and publisher.forms.testuploadfield.

The RTD imported pasted images by asking the server to fetch their URLs
(getImgFromRemoteURL), which let any client make the server fetch arbitrary
URLs. The browser now downloads pasted images itself and uploads them through
getUploadedFileFinalURL, and the server-side fetch is removed.

- data: URLs are decoded directly as the CSP generally blocks fetching them
- When 'Copy image' offers both a remote URL and the image file, use the file
- Accept png, jpeg, gif, webp and avif, max 20MB, and verify they decode
- Upload placeholders take the pasted image's dimensions
- Server-side RTD cleanup now properly discards invalid or non-image data: URLs
@unilynx
unilynx requested a review from a team November 12, 2026 12:02
@unilynx

unilynx commented Nov 12, 2026

Copy link
Copy Markdown
Member Author

@webharerob decodeImageDataURL is added as internal api - could be moved to @webhare/upload next to getFileAsDataURL and then also used to replace dataURItoBlob in imgeditcontroller.ts. worth it?

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.

🟡 Changes recommended

The paste handler can discard valid images, retain unused files and object URLs, and buffer oversized downloads before enforcing the size limit.

3 open issues
What changed in this PR

Moves pasted RTD image downloads into the browser and reuses the existing upload service, removing the unauthenticated server-side URL-fetching endpoint that enabled SSRF.

Changes:

  • Adds browser-side image validation, direct data URL decoding, and clipboard-file handling.
  • Preserves pasted image dimensions during upload and supports WebP/AVIF image-cache URLs.
  • Fixes invalid data URL cleanup and adds paste regression tests.
File Description
whtree/​modules/​webhare_testsuite/​tests/​tollium/​richdoc/​teststructured.ts Tests rejected images and PNG/WebP clipboard imports.
whtree/​modules/​tollium/​web/​ui/​components/​richeditor/​richeditor.scss Prevents upload spinners from stretching.
whtree/​modules/​tollium/​web/​ui/​components/​richeditor/​internal/​structurededitor.tsx Prefers clipboard image files and preserves styled dimensions.
whtree/​modules/​tollium/​web/​ui/​components/​richeditor/​internal/​editorbase.tsx Downloads, validates, and uploads pasted images in the browser.
whtree/​modules/​publisher/​lib/​internal/​rtd/​service.whlib Discards invalid and non-image data URLs before storage.
whtree/​modules/​publisher/​lib/​internal/​forms/​rpc.whlib Removes remote fetching and routes WebP/AVIF uploads through the image cache.
whtree/​jssdk/​forms/​src/​formservice.ts Removes the remote-image download method from the service interface.

Review effort: Balanced

Comment on lines +329 to +330
const bytes = match[2] ? Uint8Array.from(atob(decodeURIComponent(match[3])), c => c.charCodeAt(0)) : new TextEncoder().encode(decodeURIComponent(match[3]));
return new Blob([bytes], { type });
if (contentlength > pastedImageMaxSize)
throw new Error(`Image is too large (${contentlength} bytes)`);

return await response.blob();
Comment on lines +2117 to +2118
const url = URL.createObjectURL(file);
this.pastedfiles.set(url, file);
- Don't crash when clipboardData has no files (fixes testobjects-block/inline)
- Percent-decode non-base64 data: URLs directly into bytes instead of through
  decodeURIComponent, which rejects non-UTF-8 data such as image bytes
- Enforce the pasted image size limit while reading the response, as
  Content-Length may be absent
- Release object URLs for clipboard images that weren't imported, and any
  remaining ones when the editor is destroyed
- Move the pasted image helpers to a type-checked utils.ts

@webharebot webharebot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Auto-Approved: PR author (@unilynx) is an internal organization/repository member.

This branch has not been deployed

No deployments
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.

2 participants