Skip to content

fix: close script injection paths and protect masked settings - #313

Merged
MrYuion merged 10 commits into
developfrom
fix/security-hardening
Sep 30, 2026
Merged

MrYuion merged 10 commits into
developfrom
fix/security-hardening

Conversation

@MrYuion

@MrYuion MrYuion commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

A review found several ways to run script or leak secrets:

  • The extension outlet put the ?embed= query param into an iframe src through bypassSecurityTrustResourceUrl. A link with embed=javascript:... ran script in the backoffice origin. Its message listener also accepted messages from any window.
  • Driver, repository and extension links used safe: 'url', so a stored javascript: URL ran when an admin clicked it.
  • Repository URIs showed Git credentials in the link href.
  • The signage plugin iframe had allow-scripts allow-same-origin. Plugins load from the same origin, so the sandbox did nothing.
  • The settings form saved on a bare A key press. It saved every level, including masked <MASKED> secrets, which overwrote the stored values.
  • Names went into confirm dialogs and search results as HTML without escaping.

This is part of a stack of 6 PRs from a review of the whole app. Merge them in order: confirm-cancel, security, data loss, broken features, async errors, cleanup. Each PR targets the branch before it, so the diff shows only its own changes.

Fix

  • The extension outlet loads an embed URL only if it is http(s) and matches a configured extension for the item. It accepts messages only from its own iframe and replies to that origin.
  • Remove the safe: 'url' bypass. validateURI requires scheme:// and rejects javascript:, vbscript: and data:. The driver form shows the error.
  • maskUriCredentials removes the user and password from repository URIs.
  • Drop allow-same-origin from the plugin sandbox.
  • Settings save and clear use Alt+Shift+A and Alt+Shift+C. Save all writes only changed levels the user can edit. It refuses to save masked secrets.
  • Hotkeys ignore unlisted modifiers (Cmd+E no longer opens edit) and work in any key order. They only fire for the top dialog, or for the page when no dialog is open.
  • Add escapeHtml and use it for names in confirm dialogs, the rich text editor and item search results.
  • Remove the unused ?x-api-key= handler. With ignore_api_key: true the key did nothing, but it set trusted=true.

Behaviour changes: URIs without // (for example localhost:80 or mailto:) fail validation. Signage plugins cannot use localStorage or cookies.

Checks

  • tsc, bun run lint, bunx vitest run (994 tests) pass. New tests cover the URL checks, credential masking, hotkey rules and the settings form.
  • All 6 branches were merged and tested in a browser against a local PlaceOS stack (nightly images, with core and triggers). Test agents created their own records, checked results through the API, and removed the records after. embed=javascript: gave a blank frame with no alert. Plugin frames had origin=null. Masked secrets stayed encrypted after save attempts. Injected <img> names showed as text with no network request.

Made by Claude Opus 5.5 (1M context) in Claude Code (T3 Code).

🤖 Generated with Claude Code

MrYuion and others added 10 commits September 30, 2026 13:11
Confirm modal content is rendered with innerHTML, so resource names
from the API could inject markup. Add an escapeHtml helper and use it
where callers build HTML strings. Escape uploaded file names before
SunEditor insertHTML, and destroy the editor on re-init and destroy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
setupPlace passes ignore_api_key, so ts-client never uses a stored API
key. The handler only wrote the key and trusted=true to localStorage,
and the trusted flag changes the OAuth flow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Driver default_uri, repository uri and extension url links used the
safe 'url' pipe, which let javascript: URLs through. Let Angular
sanitise them instead. Reject script schemes in validateURI, and strip
repository credentials with the URL API for both link text and href.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plugin URIs are usually same origin, so allow-same-origin with
allow-scripts gave plugins full access to the backoffice session.
Drop allow-same-origin, match plugin messages on event.source as the
probe already does, and allow only http(s) plugin URIs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Single key hotkeys like KeyE fired, and called preventDefault, on
Cmd/Ctrl+E. Skip a key event when Control, Alt or Meta is held and no
combination for that key includes the modifier. Also use
isContentEditable to detect editable focus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The settings form had bare A and C hotkeys for save all and clear, and
save all wrote every level, including masked encrypted (level 3) YAML,
which could overwrite secrets. Move the hotkeys to Alt+Shift, save only
dirty levels the user can edit, and refuse to save encrypted settings
that still hold masked values.

Also mask admin and support levels for lower privilege users (is_admin
and is_support were not called), fix the save guard, push the setting
found by level, and return new arrays from signal updates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The embed query param went straight into the iframe src through a
resource URL bypass, so embed=javascript:... ran script in our origin.
Load it only when it is http(s) and matches an extension URL configured
for the item.

Accept messages only from the frame window and origin, reply to that
origin instead of '*', and ignore invalid JSON. Handle each message
directly, as the per-action timeout dropped all but the last message
of each type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
validateURI read the host of `example.com:8080` as a URI scheme, so bare
host:port values passed while `192.168.1.1:80` failed. Require an
explicit `scheme://` form and keep blocking script schemes.

Show the URI error on the driver form, and only validate the module URI
for service and websocket modules, where the field is visible.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Page hotkeys fired under an open dialog, so "e" in the New User dialog
opened an Edit dialog on top. A listener now belongs to the dialog that
is on top when it registers (or the page) and fires only while that
layer is on top.

Modifier state now comes from the key event flags, so Shift then Alt
then A triggers Alt+Shift+A.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Option labels are rendered with innerHTML, so a name like `<img src=x>`
rendered an image. Escape names and details before building the HTML,
close the detail span, and keep the `<Unnamed>` fallback visible.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
backoffice Ready Ready Preview Sep 30, 2026 3:22am UTC

Base automatically changed from fix/confirm-cancel-deletes to develop September 30, 2026 04:37
@MrYuion
MrYuion merged commit cfc2be4 into develop Sep 30, 2026
3 of 5 checks passed
@MrYuion
MrYuion deleted the fix/security-hardening branch September 30, 2026 04:45

This branch was successfully deployed

1 active deployment
Preview — a9a7fa75 Deployed Sep 30, 2026 by vercel[bot]
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