Missing Origin Validation on WebSocket Endpoint in realtime.amplenote.com May Allow Cross-Site WebSocket Hijacking (CSWSH)

linkResolution: ❌

Reports that cannot be reproduced

realtime.amplenote.com already enforces an origin allowlist

websocket connection requires a valid unique connection token


linkReport

Summary
A vulnerability exists due to missing Origin header validation on the Socket.io WebSocket upgrade handshake. The server returns 101 Switching Protocols for arbitrary attacker-controlled Origin values. If the realtime session authenticates via browser-carried credentials (session cookie with SameSite=None, or a token the browser attaches automatically), an attacker can initiate a cross-site WebSocket connection that piggybacks on the victim's session, potentially leading to unauthorized read/write of note-sync events and data exposure.
 
Target url
https://realtime.amplenote.com/socket.io/?EIO=4&transport=websocket
 
Vulnerable endpoint(s) and parameter(s)
GET /socket.io/?EIO=4&transport=websocket
Parameter(s): Origin (request header — not validated)
 
Vulnerability class
CWE-346: Origin Validation Error (also mapped to CWE-1385: Missing Origin Validation in WebSockets / CWE-352: Cross-Site Request Forgery via CSWSH)
 
Cvss 3.1 score
8.6 - High
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
 
Root cause analysis
The Socket.io server on realtime.amplenote.com performs the WebSocket Upgrade handshake without validating the Origin header. Socket.io's origin allow-list is either unset or does not apply to the WebSocket transport. In a raw handshake, the server returned 101 Switching Protocols for Origin: https://evil.example.com, Origin: null, and a lookalike origin (https://evil.amplenote.com.attacker.com). A correctly configured socket.io server should reject non-allow-listed origins with 403 Forbidden during allowRequest. This defense is absent here.
 
 
Attack scenario
Preconditions: (1) The victim is logged into Amplenote in a browser; (2) the realtime connection authenticates using credentials the browser sends automatically (session cookie with SameSite=None, or an ambient auth header). (Note: this final precondition — cookie-carried auth — requires one confirmation step against a live test account; see "Remediation status".)
An attacker hosts a page at https://evil.example.com containing JavaScript new WebSocket("wss://realtime.amplenote.com/socket.io/?EIO=4&transport=websocket"). When the victim visits the page, the browser performs the WebSocket handshake to Amplenote's realtime endpoint. Because the server does not validate Origin, the connection is accepted and the victim's authenticated realtime session is reused. The attacker's page can then send Socket.io events and read pushed note-sync events as the victim.
 
Steps to reproduce
1. Open a raw TLS connection to realtime.amplenote.com:443.
2. Send a WebSocket upgrade request with an arbitrary Origin header (e.g., https://evil.example.com).
3. Observe the response — the server returns 101 Switching Protocols instead of 403.
 
Http request
GET /socket.io/?EIO=4&transport=websocket HTTP/1.1
Host: realtime.amplenote.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://evil.example.com
 
Http response
HTTP/1.1 101 Switching Protocols
(Additionally, the polling transport returns access-control-allow-credentials: true — while it correctly omits a reflected access-control-allow-origin for evil origins, the WebSocket upgrade path enforces no origin restriction at all.)
 
 
Impact
As an attacker, I can exploit this vulnerability to:
- Hijack a victim's realtime note-sync session and read pushed events (potential note-content disclosure).
- Inject forged events/messages into the victim's realtime channel (potential data integrity violation).
- Perform actions as the victim within the realtime service.
Depending on the application context, this may result in unauthorized data access, privilege escalation, or account takeover.
 
 
Recommendation
Enforce an Origin allow-list on the Socket.io server. For Socket.io, configure cors.origin and/or an allowRequest callback that validates the Origin header against the set of trusted origins (e.g., https://www.amplenote.com, https://app.amplenote.com) before accepting the upgrade:
const io = require("socket.io")(server, {
cors: { origin: ["https://www.amplenote.com", "https://app.amplenote.com"], credentials: true },
allowRequest: (req, cb) => {
const allowed = ["https://www.amplenote.com", "https://app.amplenote.com"];
const origin = req.headers.origin;
cb(null, !origin || allowed.includes(origin));
}
});
Additionally, verify the realtime service authenticates each connection using a per-connection token that is not automatically replayed by the browser.
 
 
Exploitable poc
# Raw handshake proving missing Origin validation (Python)
import socket, ssl, base64, os
key = base64.b64encode(os.urandom(16)).decode()
req = (
"GET /socket.io/?EIO=4&transport=websocket HTTP/1.1\r\n"
"Host: realtime.amplenote.com\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: " + key + "\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Origin: https://evil.example.com\r\n\r\n"
)
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
s = ctx.wrap_socket(socket.create_connection(("realtime.amplenote.com", 443), timeout=10), server_hostname="realtime.amplenote.com")
s.send(req.encode()); print(s.recv(4096).split(b"\r\n")[0].decode()) # -> HTTP/1.1 101 Switching Protocols
A full end-to-end hijack PoC requires confirming the auth model with a test account (browser PoC: new WebSocket("wss://realtime.amplenote.com/socket.io/?EIO=4&transport=websocket") while authenticated, then observe received events).
 
References
- https://cwe.mitre.org/data/definitions/1385.html
- https://cwe.mitre.org/data/definitions/346.html
- https://portswigger.net/web-security/websockets/cross-site-websocket-hijacking
- https://cheatsheetseries.owasp.org/cheatsheets/WebSocket_Security_Cheat_Sheet.html