Skip to content

feat(lightning-message-channel): flag message channels exposed to every namespace - #114

Draft
cclabsnz wants to merge 2 commits into
mainfrom
feat/lightning-message-channel-check
Draft

cclabsnz wants to merge 2 commits into
mainfrom
feat/lightning-message-channel-check

Conversation

@cclabsnz

Copy link
Copy Markdown
Owner

Adds check 93. Came out of asking whether Lightning Message Channels — server-defined metadata, consumed entirely client-side — can be used for injection. They can, and the platform documents enough to make it auditable.

What the exposure actually is

Lightning Message Service is a browser-side bus with no server mediation. A channel with isExposed=true is published to components in any namespace, including every installed managed package. Any component sharing a page with a publisher or subscriber can:

  • subscribe and read every payload — sharing rules, OWD and FLS govern records and do not reach the bus, so the payload is readable regardless of what the reader is entitled to see;
  • publish a payload of its own, feeding a subscriber attacker-controlled input.

The second is the injection you would not expect: a subscriber that takes a record id from a message and passes it to Apex, or renders it into the page, is trusting a publisher it cannot authenticate. The bus is shaped like a trust boundary and is not one.

Three facts from the docs that shape the finding

None are obvious from the setting itself, and each changes the advice:

  1. Salesforce's own AppExchange Security Review requires isExposed=false. A defect by the platform owner's definition, not only ours.
  2. isExposed cannot be set back to false once true. Remediation is a new channel plus migrating every publisher and subscriber — not a toggle. The remediation text says so, because implying otherwise understates the cost by a wide margin.
  3. Visualforce supports only exposed channels. An org using LMS from a Visualforce page has an exposed channel because the platform requires one. "Just turn it off" would be wrong advice there, so the detail names it.

Installed channels are reported separately at LOW: identical reach, but not editable here and not reversible anyway, so there is no fix available in this org — only a question for the vendor. Reporting them together would hand a reader a remediation they cannot apply.

Verified before writing, not assumed

Queryability was the open question, and it decided the design. Confirmed against a developer edition with a read-only Tooling query:

  • LightningMessageChannel is Tooling-queryable
  • IsExposed is a selectable and filterable boolean (WHERE IsExposed = true executes)
  • ManageableState is what separates local from installed

So no MetadataClient fallback and no advisory-degradation path — the whole check is one Tooling query.

Deliberately grants no capability

An exposed channel is a surface, not reach. Nothing in the metadata says what the channel carries, so granting data-read would assert an exposure that has not been established. Wiring it into a chain as a step is a separate judgment and is not made here.

Scope left open

The check does not cross-reference which Visualforce pages or components reference each channel, which would let it escalate severity where the exposure is load-bearing. That needs a second markup scan, and VisualforceXssCheck already pays that cost under a 500-page cap — so sharing it through the cache is the right way to add it, rather than querying ApexPage twice.

Verification

Six touches per the contract: impl class, registry entry, mapping.ts control rows plus the devSecurity DOMAIN group, CheckMeta effort/impact, unit test with a mocked Tooling client, README and docs/CHECKS.md rows with the count moved 92 → 93.

npm run build clean; npm test 1250 passing across 126 suites (from 1242). Registry confirms 93 registered checks, and the exact query string the check issues was re-run against the org after the check was written.

…ry namespace

Lightning Message Service is a browser-side bus with no server mediation. A
channel with isExposed=true is published to components in any namespace,
including every installed managed package, and any component sharing a page with
a publisher or subscriber can subscribe to read every payload or publish one of
its own. Sharing rules, org-wide defaults and field-level security govern records
and do not reach it, so whatever the channel carries is readable regardless of
what the reader is entitled to see.

The matching risk is the inverse: a subscriber that treats the payload as trusted
input, taking a record id from a message and passing it to Apex, or rendering it
into the page. The bus is shaped like a trust boundary and is not one.

Three things from the platform docs shape the finding, and none of them are
obvious from the setting alone:

  - Salesforce's own AppExchange Security Review requires isExposed=false, so
    this is a defect by the platform owner's definition, not only ours.
  - isExposed cannot be set back to false once true. Remediation is therefore a
    new channel plus migrating every publisher and subscriber, not a setting
    change, and the remediation text says so rather than implying a toggle.
  - Visualforce supports only exposed channels. An org using LMS from a
    Visualforce page has an exposed channel because the platform requires one,
    not because anyone chose it, so "just turn it off" would be wrong advice.

Installed channels are reported separately at LOW. The reach is identical to a
local exposed channel, but since the exposure cannot be reversed and the metadata
is not editable here, there is no fix available in this org — only a question for
the vendor. Reporting them together would hand a reader a remediation they cannot
apply.

Queryability was verified against a developer edition before the check was
written rather than assumed: LightningMessageChannel is Tooling-queryable,
IsExposed is a selectable and filterable boolean, and ManageableState is what
separates local from installed. No MetadataClient fallback is needed, and the
check costs one Tooling query.

Deliberately grants no capability in CapabilityRegistry. An exposed channel is a
surface, not reach: nothing in the metadata says what the channel carries, so
granting data-read would assert an exposure that has not been established.
Wiring it into a chain as a step is a separate judgment and is not made here.

Scope note: the check does not yet cross-reference which Visualforce pages or
components reference each channel. That needs a second markup scan, and
VisualforceXssCheck already pays that cost with a 500-page cap, so sharing it
through the cache is the right way to add it rather than querying ApexPage twice.

Six touches per the contract: impl class, registry entry, mapping.ts control rows
plus the devSecurity DOMAIN group, CheckMeta effort/impact, unit test with a
mocked Tooling client, and the README and docs/CHECKS.md rows with the count
moved 92 -> 93.

Gates green: npm run build, npm test (1250 passing across 126 suites, up from
1242).

Signed-off-by: cclabsnz <cloudcounselnz@gmail.com>
@cclabsnz
cclabsnz marked this pull request as draft September 15, 2026 06:55
@cclabsnz

Copy link
Copy Markdown
Owner Author

Holding this as a draft. The finding text asserts that any component sharing the page can subscribe to read payloads or publish its own — that is inferred from how LMS works, not demonstrated. Establishing whether an exposed channel can actually leak data, and to whom, before this ships.

…not a wider one

The finding said any component sharing a page with a publisher or subscriber
could read every payload or publish its own. That was inferred from how a
client-side bus works and was not established, and it is wrong in the way that
matters: subscribing requires importing the channel into a deployed component,
so the actor is an installed managed package or something else deployed into the
org, never arbitrary script on the page. Written the original way it implied an
XSS-reachable bus, which the metadata does not support.

The narrower claim still stands, and on Salesforce's own account rather than
ours. Their guidance is that Lightning Message Service is secure by default,
that components in other namespaces cannot read a channel unless isExposed is
true, and that exposing it lets outside packages publish to and subscribe on it.
That is the platform owner describing the exposure, which is a better footing
than a deduction from first principles.

Two limits are now stated in the finding instead of left for a reader to infer.
The check reads channel metadata, so it establishes that the door is open and
not that anything sensitive passes through it — what a channel carries is
visible only in the publishing component. And an exposed channel in an org with
no third-party components on those pages has no audience today, though it
acquires one the moment a package is installed.

Remediation now leads with reading the publishing component, because that is
what separates urgent from untidy, and nothing in the metadata distinguishes a
channel passing a record id from one passing field values.

Both narrowed claims are pinned by tests. An overstatement is the easy drift
here: the wording that reads best is the wording that claims most.

Gates green: npm run build, npm test (1252 passing across 126 suites).

Signed-off-by: cclabsnz <cloudcounselnz@gmail.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