Skip to content

Run inline text through the setter, so scramble actually happens - #20

Open
ithiria894 wants to merge 1 commit into
acieshk:mainfrom
ithiria894:fix/scramble-inline
Open

ithiria894 wants to merge 1 commit into
acieshk:mainfrom
ithiria894:fix/scramble-inline

Conversation

@ithiria894

Copy link
Copy Markdown
Contributor

You said scramble is what blocks the heap route. It is — on one of the two ways of giving the element a secret.

<nocap-secret scramble>TEXT</nocap-secret> never scrambled

connectedCallback assigns this.#secret = inline — the private field directly — which skips set secret(value), where the whole scramble branch lives. Attribute present, element renders, nothing warns, plaintext stored in order.

Measured, with the secret assembled in JS so it is never a literal in the page source:

delivery scramble plaintext in heap #secret #chars/#slots readable rebuilt
markup off True 'PASS-7391' False —
markup on True 'PASS-7391' False —
property off True 'PASS-7391' False —
property on False '' True 'PASS-7391'

Row two is the bug. After the fix, markup matches property on both rows.

What scramble does and does not buy, now that it runs

It does defeat the cheap attack. A plain string search of a 27MB snapshot finds nothing. That is real and it is the attack most people would try first.

It does not survive the console. Runtime.getProperties returns private fields, which is exactly what expanding the element in DevTools does. #chars and #slots both read, and zipping them is one line:

#chars  ["S","1","7","S","3","P","A","9","-"]
#slots  [2,8,5,3,6,0,1,7,4]
        -> PASS-7391

Your own comment already says this — "anyone who reads both reconstructs the value immediately" — so this is confirmation rather than news. The cost to an attacker goes from Ctrl+F to knowing which two fields to look at.

Two things I would consider separately

#secret and #slots could be one field. #slots is only ever used to put the glyphs back. If the shuffle order lived somewhere less discoverable than a sibling field on the same object, the console route would need more than expanding one node. Not a fix, just a higher wall.

A warning when scramble is set but inert. The element already warns for the fake/scramble clash and for letterSpacing support. This was the same shape of silent-inert and had no warning, which is why it lasted.

Coverage

None of the 65 tests could have caught this, because the scramble branch needs an element and the runner has no DOM. That is the second defect today invisible to a green suite for the same reason — the other was mine, an opts.gamma in a method with no opts, which passed everything and threw on first render.

Might be worth a minimal DOM shim in the test setup. The element is the part of this library that most benefits from being wrong loudly.

Verified in Chrome on both paths: analysis/scramble_reach.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_013uZYmyLEmJLTjJXiAUC3wj

`<nocap-secret scramble>TEXT</nocap-secret>` never scrambled anything.

connectedCallback assigned `this.#secret = inline`, the private field directly,
which skips `set secret(value)` where the entire scramble branch lives. The
attribute was present, the element rendered, nothing warned, and #secret held
the plaintext in order. A security feature silently inert on one of its two
documented usage paths.

Measured before, secret assembled in JS so it is never a literal in the source:

  delivery   scramble   in heap   #secret        fields   rebuilt
  markup     off        True      'PASS-7391'    False    -
  markup     ON         True      'PASS-7391'    False    -     <- inert
  property   off        True      'PASS-7391'    False    -
  property   ON         False     ''             True     'PASS-7391'

And after, with markup now matching property on both rows.

The fix is the setter rather than the field. Its render() is a no-op here
because #flicker does not exist yet, and the explicit render further down still
runs, so ordering is unchanged. textContent is cleared first so the setter
cannot observe it.

Worth saying plainly: none of the 65 tests could have caught this, because the
scramble branch needs an element and the runner has no DOM. That is the second
defect today that was invisible to a green suite for the same reason.

Verified in Chrome, both delivery paths, scramble on and off:
analysis/scramble_reach.py in the audit repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uZYmyLEmJLTjJXiAUC3wj

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.

1 participant