Summary
During testing of the CRIMS client portal, a Pusher application secret was found hardcoded in a client-side JavaScript bundle. Pusher secrets are meant to stay server-side — they’re used to sign authenticated channel subscriptions and trigger events. With the secret exposed, it’s possible to independently compute valid event signatures and publish forged events directly to Pusher’s API, without ever touching the application’s backend.
The MFA flow subscribes to a public (unauthenticated) Pusher channel to listen for the user’s OneTouch approval/denial from their phone. Because public channels require no per-client authentication to listen, and the leaked secret allows anyone to publish, an attacker who knows or can guess the channel name can forge an “approved” event and potentially bypass 2FA entirely.
Attack Lifecycle
- Attacker reviews the client portal’s JS bundle and extracts the Pusher
app_id,key, andsecret - Attacker initiates a login or password-reset flow for a target account, triggering an Authy OneTouch push to the victim’s phone
- Attacker identifies the Pusher channel name used for that session (often derived from a predictable value — user ID, session ID, or similar)
- Using the leaked secret, attacker computes a valid HMAC signature and publishes a forged
approvedevent to that channel via Pusher’s REST API - The client-side app receives the forged event and proceeds as if the user approved the push on their device
Proof of Concept
Request used to trigger the OneTouch challenge:
POST /api/auth/mfa/onetouch/initiate HTTP/1.1
Host: sso.cytonn.com
Content-Type: application/json
{"user_id": "<target_user_id>"}
Extracted from the client bundle (app.js, minified, variable names shortened for illustration):
const PUSHER_CONFIG = {
app_id: "REDACTED",
key: "REDACTED",
secret: "REDACTED" // <-- should never be client-side
};
Forged event published directly to Pusher’s REST API using the leaked secret (pseudocode — actual PoC script omitted from this write-up):
import pusher
client = pusher.Pusher(
app_id=PUSHER_CONFIG["app_id"],
key=PUSHER_CONFIG["key"],
secret=PUSHER_CONFIG["secret"],
cluster="ap1"
)
client.trigger(
channel="onetouch-<target_session_channel>",
event="approval-status",
data={"status": "approved"}
)
Two Possible Outcomes
| Scenario | Impact |
|---|---|
| Backend independently re-verifies the approval with Authy’s server before granting access | Client-side forgery is cosmetic only — no real bypass, but the secret leak is still a finding worth fixing |
| Backend trusts the Pusher event itself as proof of approval | Full MFA bypass — attacker completes login or password reset without the victim’s device ever approving anything |
Testing confirmed which of these applies to CRIMS — see Validation section in the internal report for the confirmed outcome and supporting evidence.
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N — (adjust based on confirmed outcome above)
Remediation
- Never ship Pusher app secrets client-side. Only the
key(public) belongs in the frontend bundle; thesecretmust stay server-side, used only to sign server-issued authenticated channel subscriptions. - Switch the OneTouch approval channel from public to private or presence, requiring server-signed authentication to subscribe or publish.
- The backend must independently verify approval status with Authy’s API before granting access — never trust a client-observed Pusher event as sole proof of MFA approval.
- Rotate the leaked secret immediately following remediation.
Timeline
- Discovery: 2026-07-27
- Reported: 2026-07-30
- Status: Pending remediation