GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — tinyjsapp-studio/test/adversarial at main · slabbdev/tinyjsapp-studio

Expand file tree

/

adversarial

/
Copy path

Directory actions

More options

More options

Directory actions

More options

More options

Latest commit

 

History

History
History
/

adversarial

/
Copy path

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
site
site
 
 
README.md
README.md
 
 
run.sh
run.sh
 
 
serve.mjs
serve.mjs
 
 

README.md

Outline

Adversarial suite — outside-in proof for the wrapper threat model

A deliberately hostile page, wrapped and run exactly like any app the Studio generates, attacking the gates it lives behind. It is the standing, outside-the-runtime-CI companion to docs/THREAT-MODEL.md: every probe maps to a guarantee (or a known open item), and every row links the tinyjs issue/release behind it.

Scope note: the runtime audit (#26/#29/#30…) tested tinyjs from the inside. This suite tests the wraps the Studio ships — the wrapper preset, the injected page, cross-origin subframes, and the effects a page cannot hide from the outside.

Run it

sh test/adversarial/run.sh          # ~28s; a probe window appears briefly
# or: TINYJS_BIN=/path/to/tinyjs sh test/adversarial/run.sh 30

The harness (run.sh) starts a two-port static server (serve.mjs — 8787 is the wrapped origin, 8788 exists so the suite's iframe is genuinely cross-origin), runs tinyjs wrap on the hostile site into a temp dir, runs tinyjs dev for a few seconds, then checks externally. Three evidence channels, on purpose:

  1. Filesystem — the printToPDF pair: T8 writes into temp (the allowed zone since the 0.48 #36 fix — expected present), T8b aims at $HOME (must NEVER appear; since 0.48 that call opens a save panel, which also stalls the launcher's wire pump — so T8b always fires last, after the verdict relay is out).
  2. Server log — which URLs the page/popup actually reached: a GET /etc/passwd on the wrapped origin proves the file: popup was re-keyed (no disk access); a GET /exfil proves the javascript: popup executed.
  3. Clipboard — the page relays its full verdict table out via clip.write, an allowed verb of the wrapper preset (macOS pbpaste; ⚠️ it clobbers your clipboard during the run).

Verdicts also render in the probe window's table. Each run wraps under a unique app title — dev webviews persist a per-title WKWebsiteDataStore, so a repeated title replays last run's cached probe scripts. Artifacts stay in the harness's /tmp/tjs-adv-* dir (wrap.log, dev.log, serve.log).

Observed results — verified run 2026-10-06 (tinyjs v0.50.0)

id attack result evidence
T1 fs.read from the wrapped origin PASS — blocked: "fs.read" is disabled by tinyjs.json "api" gate, 0.38+
T2 clip.read PASS — blocked (write is in, read is out) gate
T3 debug.get PASS — blocked 0.42.1 fix
T4 spotlight.index PASS — blocked gate
T5a win.open('file:///etc/passwd') PASS — re-keyed: popup nav rewritten to http://<origin>/etc/passwd, 404, disk untouched serve.log + dev.log [nav]
T5b win.open('javascript:fetch(…/exfil)') INFO — same-origin execution: the popup runs in the opener's origin (no privilege gain — same wrapper gate). The 0.46 win.open screening covers the bridge path, not DOM popups — re-verified on 0.50, flagged upstream serve.log /exfil hit
T5c/d ../ walk, UNC path manual — handle returned, nothing fetched from a stranger host serve.log
T6 cross-origin iframe (port 8788) calling the bridge PASS — subframe isolated: stronger than expected — window.tiny is not even injected into the stranger-origin subframe page verdict relayed via clipboard
T6b cross-origin iframe calling the RAW WebKit handler (window.webkit.messageHandlers.tiny, the seq:json wire format directly) PASS — gated, proven live: fired post-report by parent trigger; the backend stamps the call with the iframe's OWN origin — tinyjs: denied "shell.open" for subframe http://127.0.0.1:8788 (tinyjs.json "api".origins) — and the shell.open decoy (/raw-leak) never reaches the decoy server. Probes the exact macOS vector the 0.50.1 release gated dev.log denial + serve.log absence
T7 tiny.store __proto__/constructor pollution PASS — inert data: keys persist as plain data, Object.prototype untouched (the 0.45 fix sanitizes rather than rejects) page read-backs
T8 win.printToPDF into temp PASS — allowed zone: direct write, by design since the 0.48 #36 fix harness filesystem check
T8b win.printToPDF at a protected path ($HOME) PASS — protected: no file ever appears; save panel opens (and dies with the teardown) harness filesystem check
T9 postMessage spoof barrage incl. fake win.close PASS — no effect: app alive through the barrage; the backend is socket-only harness liveness check
T10 15 malformed wire payloads (junk shapes around an allowed verb, one with a spoofed origin field) PASS — dropped or rejected, nothing executes (0.47.1 fix); the spoofed-origin canary is denied on the frame's real origin harness liveness check

Runtime quirk surfaced by T6b (flagged upstream via the maintainer, not a security issue): a DENIED raw subframe call is followed by dev-app termination — ~15–30 s later on the pre-0.50.1 build, within seconds on 0.50.1. That is why T6b fires LAST, after the verdict report has relayed, and is judged by external evidence (the dev.log denial line vs a server log leak hit). The harness classifies late death as LIVENESS INFO (report relayed) versus LIVENESS FAIL (death with no report — the signature of an executed spoofed verb).

Manual click-throughs (need eyes, not a harness)

  • External-link policy: wrap with --external trusted.example, click a link to a listed domain (must open in the default browser) and an unlisted one (must stay in-app).
  • Redirect re-keying: wrap google.fr; let it redirect to consent.google.com — the gate must re-key (it does); the frameless drag strip's window verbs die (known Studio limitation, ROADMAP Phase 3 #14 — fix proposed upstream: window chrome belongs in API_ALWAYS).
  • Built-app env scrubbing: build a wrap, then from the page dump process-adjacent env via the bridge — TINYJS_*/WEBVIEW2_* must be absent in the built app (they are present under tinyjs dev, which is a developer context, by design).
  • Media consent: wrap a getUserMedia page — camera/mic must go through the gate and surface a consent path, never auto-grant (#24 tracks the bundled-app case).

Reading a result

  • PASS is the good column. FAIL — Object.prototype polluted, FAIL — PROTECTED PATH WRITTEN or an executed escape is a finding: file it upstream (private advisory for exploitable classes — see SECURITY.md), never a public issue.
  • INFO rows are behavioral observations with no privilege gain — worth telling upstream, not worth alarming users.
  • CHECK manually means neither the page nor the harness could observe the outcome alone — the manual list above decides it.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.