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:
- 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).
- 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.
- Denial of service. A forged
web-eid:<action>-failure aborts a
legitimate authentication/signing flow with an arbitrary error.
- 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.
- 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.
Security Advisory: Missing sender validation in Web eID library message receiver
@web-eid/web-eid-library(web-eid.js)src/services/WebExtensionService.tsAV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:L)Summary
The library's
windowmessagelistener accepts and acts on any message whosedata.actionis a string starting withweb-eid:, without validatingevent.sourceorevent.origin. Any code able to deliver amessageevent tothe 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(), andstatus()promises.This is inconsistent with the project's own browser extension content script
(
web-eid-webextension), which already enforcesevent.source === windowbefore relaying messages. The in-page library applies no equivalent check.
Details
src/services/WebExtensionService.ts: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 topending requests only by action name, with no per-request nonce.
Consequences for an attacker holding any in-page scripting context:
web-eid:<action>-successresolves the page's pending promise with attacker-chosen
unverifiedCertificate/signaturevalues(
src/web-eid.ts,authenticate/sign).before the genuine extension response pre-empts it and silently drops the
real one.
web-eid:<action>-failureaborts alegitimate authentication/signing flow with an arbitrary error.
web-eid:<action>-ackcancelsackTimer, suppressing theExtensionUnavailableErrorthat normally fires withinEXTENSION_HANDSHAKE_TIMEOUTand holding the flow open.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 (
MessageEventcrafted with a foreign originand a
sourcethat is notwindow):The forged failure variant rejects the pending
sign()promise with a spoofedERR_WEBEID_USER_CANCELLED. Both cases pass against v2.1.0, confirming noorigin/source validation is performed.
Remediation
Validate the sender at the top of
receive(). Legitimate responses are postedby the content script into the same window, so both checks are safe:
Defense-in-depth hardening, recommended additionally:
to prevent same-origin response forgery and races.
web-eid:warningbranch, guard withArray.isArray(message.warnings)and
typeof warning === "string", and cap theloggedWarningscollection.As written it accepts unbounded unique strings (O(n²) via
Array.includes)and throws on a non-array / non-string
warningsvalue — a secondaryclient-side DoS (CVSS 3.1 5.3,
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L).deserializeError()(src/utils/errorSerializer.ts), build the error froman 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 copyinguntrusted keys is unnecessary.)
Disclosure
Reported by OffSeq Cybersecurity under coordinated disclosure.