feat(compiler): SYN027 — warn on bare postMessage() cross-origin messaging bypass (?bs 0.7+, closes #180)#182
Open
marcelofarias wants to merge 5 commits into
Open
feat(compiler): SYN027 — warn on bare postMessage() cross-origin messaging bypass (?bs 0.7+, closes #180)#182marcelofarias wants to merge 5 commits into
marcelofarias wants to merge 5 commits into
Conversation
… capability model (?bs 0.7+, closes #180) Bare postMessage(data, origin) sends data cross-origin to another browsing context, a network-equivalent dependency invisible to botscript's capability model. No uses {}, reads {}, or writes {} declaration covers it. In bot/agent contexts this is a known prompt-injection exfiltration vector: injected content can call bare postMessage to leak data to an attacker-controlled origin. Detection: postMessage not preceded by ./?., followed by ( or ?.(. Excluded: member calls (worker.postMessage, iframe.contentWindow.postMessage), fn/function/function* declarations, object method shorthands, and unsafe {}/unsafe fn bodies. Relation to existing checks: SYN007 (fetch/HTTP), SYN014 (BroadcastChannel/same-origin broadcast), SYN027 (cross-origin messaging) — completes the net-bypass family. Wired through error-codes, MCP explain, tests (13 cases), AGENTS.md, README.md, and examples/react-app/src/messaging.bs. Co-Authored-By: Botkowski <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new non-blocking compiler structural warning (SYN027) to flag bare postMessage() calls inside ?bs 0.7+ function bodies, surfacing cross-origin messaging as a capability-model bypass. The change extends the existing SYN00x “ambient/global bypass” family and wires the new code through diagnostics, MCP explanations, tests, and docs.
Changes:
- Implement
SYN027detection insyn-checkto warn on barepostMessage(data, origin)/postMessage?.(data, origin)outsideunsafe. - Register
SYN027in the diagnostic registry and MCPexplaincontent, and update allowlists/tests. - Add a React example file intended to demonstrate the preferred/unsafe patterns.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds SYN027 to the documented MCP explain supported-code list. |
| AGENTS.md | Adds SYN027 to the diagnostics table. |
| packages/mcp/tests/server.test.ts | Adds SYN027 to the MCP KNOWN_CODES contract list. |
| packages/mcp/src/explanations.ts | Adds long-form MCP explanation content and examples for SYN027. |
| packages/compiler/tests/syn027-check.test.ts | Adds compiler tests covering positive/negative detection cases for SYN027. |
| packages/compiler/tests/error-codes.test.ts | Adds SYN027 to the compiler diagnostic allowlist. |
| packages/compiler/src/passes/syn-check.ts | Implements the SYN027 token-scan detection and warning emission. |
| packages/compiler/src/error-codes.ts | Registers SYN027 rule/idiom/rewrite/example in the error-code registry. |
| examples/react-app/src/messaging.bs | Adds an example showing parameter-passing and unsafe escape-hatch usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+1705
to
+1707
| // Exclude: member calls — `worker.postMessage(...)`, `window.postMessage(...)` | ||
| if (prev27 && ((prev27.kind === "punct" && prev27.text === ".") || prev27.kind === "questionDot")) | ||
| continue; |
Comment on lines
+1
to
+5
| ?bs 0.7 | ||
|
|
||
| // SYN027 example: bare postMessage is invisible to the capability model. | ||
| // Pass the messaging function as a parameter so the dependency is declared. | ||
|
|
…import messaging example - SYN027 now fires on `window.postMessage(...)`, `globalThis.postMessage(...)`, and `self.postMessage(...)` — these are ambient-global spellings of the same cross-origin surface as bare `postMessage`. Only excluded when the receiver is itself a member (e.g. `obj.window.postMessage` — non-ambient handle). - Add 5 new test cases covering the three ambient spellings, the non-ambient exclusion, and the optional-chain form `window?.postMessage(...)`. - Import `./messaging.bs` as a side-effect in `examples/react-app/src/main.tsx` so the example is guaranteed to be compiled by vite (not just present in the src directory). - Update error-codes rule/title and MCP explain body to document all detected forms. Addresses Copilot review suggestions on PR #182.
Comment on lines
+1766
to
+1770
| message: | ||
| `fn '${decl.name}' calls postMessage${callSep27}() — ` + | ||
| `bare postMessage sends data cross-origin, invisible to the capability model; ` + | ||
| `pass the messaging function as a parameter so the dependency is declared in the fn header, ` + | ||
| `or wrap in unsafe "posts cross-origin message for <reason>" { postMessage${callSep27}(data, origin) }`, |
| | SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. | | ||
| | SYN022 | (0.7+, warning) A fn body accesses `process.argv`, `process.cwd()`, `process.platform`, `process.arch`, `process.pid`, `process.ppid`, `process.version`, `process.versions`, `process.hrtime()`, `process.uptime()`, `process.memoryUsage()`, `process.cpuUsage()`, or `process.resourceUsage()`. These read ambient Node.js process state at runtime — OS identity, process tree, memory, CPU — invisible to botscript's capability model. Unlike `process.env` (SYN005) and `process.exit` (SYN006), which cover configuration and termination respectively, SYN022 targets the remaining ambient introspection surface. Detection: `process` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member in the ambient-state set, optionally followed by `(` or `?.(`. Bare `process.*` references without a trailing `(` still fire. `obj.process.*` member calls, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. `process.env` and `process.exit` are handled by SYN005/SYN006 and excluded here. | Pass the required ambient value as an explicit fn parameter so the dependency is visible in the call signature and tests can inject a mock. If direct `process.*` access is required at a bootstrap entry point, wrap in `unsafe "reads process state for <reason>" { process.argv }`. | | ||
| | SYN023 | (0.7+, warning) A fn body accesses a high-concern `navigator.*` member: `geolocation`, `clipboard`, `mediaDevices`, `serviceWorker`, `permissions`, `onLine`, `userAgent`, `language`, `languages`, `platform`, `hardwareConcurrency`, `deviceMemory`, `connection`, or `wakeLock`. These expose ambient browser capability state — location, clipboard, media devices, background service workers, network connectivity, browser identity, and hardware specs — invisible to botscript's capability model. Detection: `navigator` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member in the high-concern set. `obj.navigator.*` member calls, `fn`/`function`/`function*` declarations named `navigator`, members not in the listed set, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. | Pass the required value as an explicit fn parameter so callers can see the dependency and tests can inject a mock. If direct `navigator.*` access is required, wrap in `unsafe "accesses navigator.<member> for <reason>" { navigator.<member> }`. | | ||
| | SYN027 | (0.7+, warning) A fn body calls bare `postMessage(data, origin)`, `postMessage?.(data, origin)` — any call not preceded by `.`/`?.`. Bare `postMessage` sends structured data cross-origin to another browsing context, a cross-origin communication dependency invisible to botscript's capability model (no `uses { net }`, `reads {}`, or `writes {}` covers it). In bot/agent contexts `postMessage` is a known prompt-injection exfiltration vector. Detection: `postMessage` ident not preceded by `.`/`?.`, followed by `(` or `?.(`. Excluded: member calls (`worker.postMessage`, `iframe.contentWindow.postMessage`) — these operate on an already-declared handle; `fn`/`function`/`function*` declarations named `postMessage`; object method shorthands. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. Relation: SYN007 (fetch/HTTP), SYN014 (BroadcastChannel/same-origin broadcast), SYN027 (cross-origin messaging). | Pass the messaging function as an explicit parameter so the cross-origin dependency is declared in the fn header. If direct cross-origin messaging is required, wrap in `unsafe "posts cross-origin message for <reason>" { postMessage(data, origin) }`. | |
Comment on lines
+1052
to
+1054
| SYN027: { | ||
| code: "SYN027", | ||
| title: "postMessage() cross-origin call (bare or via window/globalThis/self) bypasses the capability model", |
Comment on lines
+1341
to
+1344
| SYN027: { | ||
| code: "SYN027", | ||
| title: "postMessage() cross-origin call (bare or via window/globalThis/self) bypasses the capability model", | ||
| body: |
…mbient-global forms, numeric ordering in registry and MCP, updated AGENTS.md - Warning message now uses the actual call form: `window.postMessage()` or `globalThis.postMessage()` instead of always saying "bare postMessage" for ambient-global spellings; track receiver name at detection time - Move SYN027 entry in error-codes.ts and explanations.ts after SYN023 to restore numeric ordering within the SYN family - Update AGENTS.md SYN027 row to document both bare and ambient-global detection forms (window/globalThis/self.postMessage), excluded forms, and suppression rules - Add test: window.postMessage message must contain "window.postMessage", not "bare postMessage" - The messaging.bs example was already wired into main.tsx via side-effect import; no change needed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+1747
to
+1756
| // Exclude method shorthands and TS method signatures: | ||
| // `{ postMessage(data) { ... } }` / `{ postMessage(data): void; }` | ||
| if (afterTok27.matchedAt !== undefined) { | ||
| const afterParen27 = tokens[nextSignificant(tokens, afterTok27.matchedAt + 1)]; | ||
| if ( | ||
| afterParen27 && | ||
| ((afterParen27.kind === "open" && afterParen27.text === "{") || | ||
| (afterParen27.kind === "punct" && afterParen27.text === ":")) | ||
| ) continue; | ||
| } |
Comment on lines
+1712
to
+1714
| if (prev27 && ((prev27.kind === "punct" && prev27.text === ".") || prev27.kind === "questionDot")) { | ||
| const AMBIENT_GLOBALS_27 = new Set(["window", "globalThis", "self"]); | ||
| const prevPrevIdx27 = prevSignificant(tokens, prevIdx27 - 1); |
Comment on lines
+118
to
+126
| it("does NOT fire on function keyword declaration named postMessage", () => { | ||
| const src = | ||
| "?bs 0.7\n" + | ||
| "fn wrapper() -> void {\n" + | ||
| " function postMessage(data: object, origin: string) { }\n" + | ||
| "}\n"; | ||
| const result = compile(src); | ||
| expect(result.warnings.some((w) => w.code === "SYN027")).toBe(false); | ||
| }); |
…27, fix ternary false negative, add generator test - Hoist AMBIENT_GLOBALS_27 to module level (was re-allocated per token) - Remove ':' from method-shorthand exclusion: it caused false negatives for postMessage() in ternary consequents (cond ? postMessage(...) : x) - Add function* generator declaration exclusion test - Add ternary-consequent regression test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+1757
to
+1775
| const callSep27 = isOpt27 ? "?." : ""; | ||
| // Build call site description: `window.postMessage()` or `postMessage()` | ||
| const callDesc27 = ambientReceiver27 | ||
| ? `${ambientReceiver27}${ambientDot27}postMessage${callSep27}()` | ||
| : `postMessage${callSep27}()`; | ||
| const loc27 = locationOf(src, tok.start); | ||
| warnings.push({ | ||
| code: "SYN027", | ||
| severity: "warning", | ||
| file: null, | ||
| line: loc27.line, | ||
| column: loc27.column, | ||
| start: tok.start, | ||
| end: afterTok27.start + 1, | ||
| message: | ||
| `fn '${decl.name}' calls ${callDesc27} — ` + | ||
| `${callDesc27} sends data cross-origin, invisible to the capability model; ` + | ||
| `pass the messaging function as a parameter so the dependency is declared in the fn header, ` + | ||
| `or wrap in unsafe "posts cross-origin message for <reason>" { postMessage${callSep27}(data, origin) }`, |
Comment on lines
+185
to
+188
| * SYN027 A bare `postMessage(data, origin)`, `window.postMessage(...)`, | ||
| * `globalThis.postMessage(...)`, or `self.postMessage(...)` call was detected in a | ||
| * fn body (?bs 0.7+). These are all ambient-global spellings that send structured | ||
| * data to another browsing context — a cross-origin communication dependency |
…safe-wrap form - Change "sends data cross-origin" → "sends data to another browsing context" because postMessage can target same-origin contexts (same page, same worker pool). The original wording was technically inaccurate. - Fix the unsafe-wrap suggestion to mirror the actual detected call form: window.postMessage(data, origin) was suggested as postMessage(data, origin), which confused readers when the flagged call was the explicit ambient spelling. Use callDesc27.replace(/\(\)$/, "(data, origin)") so the suggestion matches the form that was actually detected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+1747
to
+1753
| // Exclude object method shorthands: `{ postMessage(data) { ... } }` | ||
| // Do NOT exclude on `:` — that would create false negatives for ternary consequents | ||
| // like `cond ? postMessage(data, origin) : other` where `:` is the ternary separator. | ||
| if (afterTok27.matchedAt !== undefined) { | ||
| const afterParen27 = tokens[nextSignificant(tokens, afterTok27.matchedAt + 1)]; | ||
| if (afterParen27 && afterParen27.kind === "open" && afterParen27.text === "{") continue; | ||
| } |
Comment on lines
+128
to
+136
| it("does NOT fire on object method shorthand { postMessage(data) { ... } }", () => { | ||
| const src = | ||
| "?bs 0.7\n" + | ||
| "fn makeHandler() -> any {\n" + | ||
| " return { postMessage(data: object) { return data } }\n" + | ||
| "}\n"; | ||
| const result = compile(src); | ||
| expect(result.warnings.some((w) => w.code === "SYN027")).toBe(false); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SYN027to the compiler's syn-check pass: fires when barepostMessage(data, origin)orpostMessage?.(data, origin)is called inside a?bs 0.7+fn body (call not preceded by./?.)postMessagesends structured data cross-origin to another browsing context — a net-equivalent dependency invisible to botscript's capability model; nouses { net },writes {}, orreads {}covers itpostMessageis a known prompt-injection exfiltration vectorworker.postMessage,iframe.contentWindow.postMessage) — these operate on an already-declared handle;fn/function/function*declarations namedpostMessage; method shorthandsunsafe {}andunsafe "reason" fnbodiesexplain, tests, and docsRelation to existing checks
fetch()— HTTP network requestBroadcastChannel— same-origin broadcastpostMessage— cross-origin messagingTest plan
postMessage(data, origin)andpostMessage?.(data, origin)at?bs 0.7+postMessage?bs 0.7, insideunsafe, member calls (worker.postMessage,iframe.contentWindow.postMessage,target?.postMessage), fn declarations, method shorthands, bare references without(packages/compiler/tests/syn027-check.test.tsexamples/react-app/src/messaging.bs🤖 Generated with Claude Code