Skip to content

Missing sender validation in message receiver allows forged extension responses (CWE-346) #84

Description

@thistehneisen

Security Advisory: Missing sender validation in Web eID library message receiver

Component @web-eid/web-eid-library (web-eid.js)
Affected version 2.1.0 (and earlier releases using the same receiver)
File src/services/WebExtensionService.ts
Class CWE-346 Origin Validation Error / CWE-940 Improper Verification of Source of a Communication Channel
Severity Medium — CVSS 3.1 4.2 (AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:L)
Researcher Nils Putnins (npu@offseq.com), OffSeq Cybersecurity — https://offseq.com

Summary

The library's window message listener accepts and acts on any message whose
data.action is a string starting with web-eid:, without validating
event.source or event.origin. Any code able to deliver a message event to
the page — an embedded iframe (e.g. a third-party ad, widget, or user-rendered
content), or a malicious parent that frames the eID page — can forge extension
responses and drive the resolution of the library's pending authenticate(),
sign(), getSigningCertificate(), and status() promises.

This is inconsistent with the project's own browser extension content script
(web-eid-webextension), which already enforces event.source === window
before relaying messages. The in-page library applies no equivalent check.

Details

src/services/WebExtensionService.ts:

constructor() {
  window.addEventListener("message", (event) => this.receive(event));
}

private receive(event: { data: ExtensionResponse }): void {
  if (typeof event.data?.action !== "string") return;
  if (!event.data.action.startsWith("web-eid:")) return;
  // ... matches a pending request by action name and resolves/rejects it
  //     with the attacker-supplied event.data

There is no check that the message originates from the same window
(event.source === window) or from the page's own origin
(event.origin === window.location.origin). Responses are correlated to
pending requests only by action name, with no per-request nonce.

Consequences for an attacker holding any in-page scripting context:

  1. Flow hijack / response injection. A forged web-eid:<action>-success
    resolves the page's pending promise with attacker-chosen
    unverifiedCertificate / signature values
    (src/web-eid.ts, authenticate/sign).
  2. Response race. Resolution is first-success-wins; a forged success posted
    before the genuine extension response pre-empts it and silently drops the
    real one.
  3. Denial of service. A forged web-eid:<action>-failure aborts a
    legitimate authentication/signing flow with an arbitrary error.
  4. Defeating extension-availability detection. A forged
    web-eid:<action>-ack cancels ackTimer, suppressing the
    ExtensionUnavailableError that normally fires within
    EXTENSION_HANDSHAKE_TIMEOUT and holding the flow open.
  5. Identity-display spoofing. Integrations that render user identity from
    the (explicitly unverified) certificate before the server round-trip will
    display attacker-controlled data.

Scope of impact

This is not a server-side authentication bypass. The authentication token's
certificate and signature are verified server-side, and the website origin is
bound by the trusted extension/native application rather than supplied by the
page. A forged in-page response cannot mint a token that passes a correctly
implemented Web eID backend validator. The realistic impact is client-side
integrity and availability: flow hijack/abort, response pre-emption, and
spoofing of any client-side trust placed in the response prior to server
verification.

Proof of concept

Added as a regression-style test (MessageEvent crafted with a foreign origin
and a source that is not window):

const pending = service.send(
  { action: "web-eid:authenticate", libraryVersion: "2.1.0" }, 60_000,
);

window.dispatchEvent(new MessageEvent("message", {
  data: {
    action:                "web-eid:authenticate-success",
    unverifiedCertificate: "ATTACKER_CONTROLLED_CERT",
    signature:             "ATTACKER_CONTROLLED_SIGNATURE",
    algorithm:             "ES256",
    format:                "web-eid:1.0",
    appVersion:            "https://evil.example/app",
  },
  origin: "https://evil.example",  // not window.location.origin
  source: {},                      // not window
}));

const result = await pending;
// result.unverifiedCertificate === "ATTACKER_CONTROLLED_CERT"  ← resolves

The forged failure variant rejects the pending sign() promise with a spoofed
ERR_WEBEID_USER_CANCELLED. Both cases pass against v2.1.0, confirming no
origin/source validation is performed.

Remediation

Validate the sender at the top of receive(). Legitimate responses are posted
by the content script into the same window, so both checks are safe:

private receive(event: MessageEvent): void {
  if (event.source !== window) return;
  if (event.origin !== window.location.origin) return;
  if (typeof event.data?.action !== "string") return;
  if (!event.data.action.startsWith("web-eid:")) return;
  // ...
}

Defense-in-depth hardening, recommended additionally:

  • Add a per-request correlation nonce, echoed by the extension and verified here,
    to prevent same-origin response forgery and races.
  • In the web-eid:warning branch, guard with Array.isArray(message.warnings)
    and typeof warning === "string", and cap the loggedWarnings collection.
    As written it accepts unbounded unique strings (O(n²) via Array.includes)
    and throws on a non-array / non-string warnings value — a secondary
    client-side DoS (CVSS 3.1 5.3, AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L).
  • In deserializeError() (src/utils/errorSerializer.ts), build the error from
    an allow-list of fields rather than copying arbitrary attacker keys via
    Reflect.set. (Note: this does not cause global prototype pollution — a
    __proto__ key re-points only the single error instance — but copying
    untrusted keys is unnecessary.)

Disclosure

Reported by OffSeq Cybersecurity under coordinated disclosure.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions