Skip to content

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
mainfrom
botkowski/syn027-postmessage
Open

feat(compiler): SYN027 — warn on bare postMessage() cross-origin messaging bypass (?bs 0.7+, closes #180)#182
marcelofarias wants to merge 5 commits into
mainfrom
botkowski/syn027-postmessage

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN027 to the compiler's syn-check pass: fires when bare postMessage(data, origin) or postMessage?.(data, origin) is called inside a ?bs 0.7+ fn body (call not preceded by ./?.)
  • Bare postMessage sends structured data cross-origin to another browsing context — a net-equivalent dependency invisible to botscript's capability model; no uses { net }, writes {}, or reads {} covers it
  • In bot/agent contexts, bare postMessage is a known prompt-injection exfiltration vector
  • Excluded: member calls (worker.postMessage, iframe.contentWindow.postMessage) — these operate on an already-declared handle; fn/function/function* declarations named postMessage; method shorthands
  • Suppressed inside unsafe {} and unsafe "reason" fn bodies
  • Wired through error-codes, MCP explain, tests, and docs
  • Closes feat(compiler): SYN027 — warn on window.postMessage/postMessage (net bypass, like SYN007 fetch) #180

Relation to existing checks

Check Bypass mechanism
SYN007 fetch() — HTTP network request
SYN014 BroadcastChannel — same-origin broadcast
SYN027 bare postMessage — cross-origin messaging

Test plan

  • Fires on postMessage(data, origin) and postMessage?.(data, origin) at ?bs 0.7+
  • Warning message includes fn name and postMessage
  • Does NOT fire: below ?bs 0.7, inside unsafe, member calls (worker.postMessage, iframe.contentWindow.postMessage, target?.postMessage), fn declarations, method shorthands, bare references without (
  • 13 test cases in packages/compiler/tests/syn027-check.test.ts
  • Full test suite passes (1493 tests across 55 test files)
  • Example in examples/react-app/src/messaging.bs

🤖 Generated with Claude Code

… 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>

Copilot AI 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.

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 SYN027 detection in syn-check to warn on bare postMessage(data, origin) / postMessage?.(data, origin) outside unsafe.
  • Register SYN027 in the diagnostic registry and MCP explain content, 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.
@marcelofarias marcelofarias requested a review from Copilot June 19, 2026 06:30
@github-actions github-actions Bot removed the request for review from Copilot June 19, 2026 06:30
@marcelofarias marcelofarias requested a review from Copilot June 19, 2026 10:27

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

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) }`,
Comment thread AGENTS.md Outdated
| 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>

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

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>

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

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>

Copilot AI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

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);
});
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.

feat(compiler): SYN027 — warn on window.postMessage/postMessage (net bypass, like SYN007 fetch)

2 participants