Skip to content

Scope camera access to QR Master instead of the whole site - #165

Merged
YurMil merged 1 commit into
mainfrom
security/scope-camera-permission
Aug 16, 2026
Merged

Scope camera access to QR Master instead of the whole site#165
YurMil merged 1 commit into
mainfrom
security/scope-camera-permission

Conversation

@YurMil

@YurMil YurMil commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Why it was site-wide

Permissions-Policy granted camera=(self) on every response. The cause sat one layer down: UtilityShellPage defaulted iframeAllow to "camera 'self'", so all 28 embedded utilities requested a camera and the header had to permit it globally for the single tool that uses one.

That tool is QR Master — it scans via html5-qrcode with facingMode: "environment" (app.html:588). The only other getUserMedia hits under static/utility-apps are Emscripten's SDL shim inside the metal-bending worker and the OpenCascade glue — dead code in both builds, sitting next to an audio MIME-type map.

The change

  • iframeAllow defaults to ''; QR Master declares camera 'self' for itself.
  • Permissions-Policy is set per path: global goes back to camera=(), QR Master's two paths get camera=(self).
  • Whisper drops camera=(self) from its own rule — it only ever needed the microphone, and carried the camera because these rules replace the global header rather than merge with it.

The header is what enforces this. A same-origin iframe inherits any feature whose default allowlist is 'self' regardless of its allow attribute, so narrowing the attribute alone would have changed nothing. It encodes intent, and starts mattering the day an app is served from another origin.

Also fixes: the locale gap

The existing whisper rules matched /utilities/whisper-transcriber/ but not /ru/utilities/whisper-transcriber/ or the other four locales — the shell page exists under all six. On those pages the global header applied, so microphone=() reached the parent document and the same-origin iframe could not hold a capability its parent lacked. The microphone was denied on five of six languages.

Both tools now match an optional locale segment. Verified against path-to-regexp 6.3.0, the version behind Vercel's routing:

path /utilities/x/:path* /:locale(ru|ua|de|es|et)?/utilities/x/:path*
/utilities/qr-master/ match match
/ru/utilities/qr-master/ no match match
/de/, /et/, … no match match
/docs/utilities/qr-master/ no match no match
/utilities/qr-nameplate/ no match no match
/xx/utilities/qr-master/ no match no match

Verification

Six-locale build, exit 0. Rendered allow attributes:

  • build/utilities/qr-master/index.html and build/ru/utilities/qr-master/index.htmlallow="camera 'self'"
  • build/utilities/whisper-transcriber/index.htmlallow="microphone 'self'"
  • build/utilities/pipe-cutter/index.htmlallow="" (previously inherited the camera)

Across all built utility pages: 26 with no delegation, 1 camera, 1 microphone. vercel.json parses as valid JSON; pnpm typecheck clean; eslint on the changed files reports only the pre-existing set-state-in-effect warnings.

Worth a preview smoke test

Header rules that replace rather than merge is the convention already in this file, not something I could verify locally. Worth opening QR Master on the preview deploy — on /utilities/qr-master/ and on /ru/utilities/qr-master/ — and confirming the scanner still gets the camera, plus whisper's microphone on a non-English page, which should now work for the first time.

🤖 Generated with Claude Code

The Permissions-Policy header granted camera=(self) on every response. The
reason was one layer down: UtilityShellPage defaulted iframeAllow to
"camera 'self'", so all 28 embedded utilities asked for a camera and the
header had to allow it site-wide for the one tool that uses it.

QR Master is that tool — it scans through html5-qrcode with
facingMode: "environment". The other two getUserMedia hits under
static/utility-apps are Emscripten's SDL shim in the metal-bending worker and
in the OpenCascade glue, dead code in both builds.

So: default the delegation to nothing, name the camera on QR Master, and set
the header per path. 26 utility pages now render allow="", one asks for the
camera, one for the microphone.

The header is the part that actually enforces this. A same-origin iframe
inherits any feature whose default allowlist is 'self' regardless of its allow
attribute, so narrowing the attribute alone would have changed nothing; it
encodes intent, and starts mattering the day an app is served cross-origin.

Also fixes a live gap in the whisper-transcriber rules, which matched
/utilities/whisper-transcriber/ but not /ru/utilities/whisper-transcriber/ or
the other four locales — so the microphone was denied on five of six
languages. Both tools now match an optional locale segment. Verified with
path-to-regexp 6.3.0, the version behind Vercel's routing: the pattern matches
all six locale paths and still rejects /docs/utilities/qr-master/,
/utilities/qr-nameplate/ and a bogus /xx/ prefix.

Whisper also no longer claims camera=(self) in its own rule; it only ever
needed the microphone, and it carried camera along because these rules replace
the global header rather than merge with it.

Verified on a six-locale build: qr-master renders allow="camera 'self'" in
both / and /ru/, whisper renders allow="microphone 'self'", and pipe-cutter —
which used to inherit the camera — renders allow="".

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

supabase Bot commented Aug 16, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project bkcimygtsnckzexbfqxh because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
cadautoscript-com Ready Ready Preview Aug 16, 2026 10:52am

@YurMil
YurMil merged commit 2fd78f2 into main Aug 16, 2026
8 checks passed
YurMil added a commit that referenced this pull request Aug 17, 2026
The Supabase check reports two different failures depending on where it runs,
and the runbook only covered one of them. On a pull request it builds a database
from the migration files and dies on public.profiles. On main it syncs to
production and dies on "Remote migration versions not found in local migrations
directory" — which it has been doing since at least 7 August, before any of the
recent work.

That second failure matters for task 1: nothing has synced to production for
weeks, so db push is not going to apply the migration cleanly the way the
document originally implied. Added the diagnosis, the migration list step, and
the choice between capturing a remote-only version and discarding the record —
with a warning against reaching for migration repair to silence the error, since
that is how the drift got this far.

Also frames the whole document around that drift: the repository and the
production database disagree in both directions, and every task here is a
symptom of it. Status table refreshed — #165 and #166 are merged, nanoid opened
a new high alert today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
YurMil added a commit that referenced this pull request Aug 17, 2026
The Supabase check reports two different failures depending on where it runs,
and the runbook only covered one of them. On a pull request it builds a database
from the migration files and dies on public.profiles. On main it syncs to
production and dies on "Remote migration versions not found in local migrations
directory" — which it has been doing since at least 7 August, before any of the
recent work.

That second failure matters for task 1: nothing has synced to production for
weeks, so db push is not going to apply the migration cleanly the way the
document originally implied. Added the diagnosis, the migration list step, and
the choice between capturing a remote-only version and discarding the record —
with a warning against reaching for migration repair to silence the error, since
that is how the drift got this far.

Also frames the whole document around that drift: the repository and the
production database disagree in both directions, and every task here is a
symptom of it. Status table refreshed — #165 and #166 are merged, nanoid opened
a new high alert today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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