Skip to content

Commit a86a2c5

Browse files
committed
Respond with 401 if wrong web socket token
commit 79e595b added a secret token which is required to connect via web socket (to prevent random websites from connecting to elm-watch). However, I somehow messed up the fix. The web socket still connects. But it is at least given a difference state, which makes the web socket connection less useful. No messages with interesting information was sent to the client, and all commands sent from the client except one were ignored. The only command the client could perform is `PressedOpenEditor`, which would execute your configured command for opening your editor. Not that harmful (unless you have an insecure editor command), but very annoying (and scary) if it would happen. This commit responds with 401 and does not initiate a web socket connection if the secret is wrong. This completely fixes the issue. It results in worse error messages if this were to happen on `localhost` for some reason. I include the header `X-Reason: Invalid token` or `X-Reason: Invalid URL`. They are intentionally vague to not expose too much to an unauthorized requester, but should help debugging slightly at least, if needed. And small info message is printed in the terminal.
1 parent b5d11f2 commit a86a2c5

4 files changed

Lines changed: 201 additions & 167 deletions

File tree

src/Errors.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,48 +1368,23 @@ ${text(error.message)}
13681368
`;
13691369
}
13701370

1371-
export function webSocketBadUrl(
1372-
expectedStart: string,
1373-
actualUrlString: string,
1374-
): string {
1375-
return `
1376-
I expected the web socket connection URL to start with:
1377-
1378-
${expectedStart}
1379-
1380-
But it looks like this:
1381-
1382-
${actualUrlString}
1383-
1384-
The web socket code I generate is supposed to always connect using a correct URL, so something is up here.
1385-
`.trim();
1386-
}
1387-
13881371
export function webSocketParamsDecodeError(
13891372
error: Codec.DecoderError,
1390-
actualUrlString: string,
1373+
urlParams: URLSearchParams,
13911374
): string {
13921375
return `
13931376
I ran into trouble parsing the web socket connection URL parameters:
13941377
13951378
${printJsonError(error).text}
13961379
1397-
The URL looks like this:
1380+
The URL parameters look like this:
13981381
1399-
${actualUrlString}
1382+
${urlParams.toString()}
14001383
14011384
The web socket code I generate is supposed to always connect using a correct URL, so something is up here. Maybe the JavaScript code running in the browser was compiled with an older version of elm-watch? If so, try reloading the page.
14021385
`;
14031386
}
14041387

1405-
export function webSocketWrongToken(): string {
1406-
return `
1407-
The web socket connected with the wrong security token. The security token is used to block malicious connections.
1408-
1409-
The web socket code I generate is supposed to always connect using the correct token, so something is up here. Maybe the JavaScript code running in the browser was compiled with an older version of elm-watch? If so, try reloading the page.
1410-
`.trim();
1411-
}
1412-
14131388
export function webSocketWrongVersion(
14141389
expectedVersion: string,
14151390
actualVersion: string,

src/Hot.ts

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as childProcess from "child_process";
22
import * as chokidar from "chokidar";
3-
import * as crypto from "crypto";
43
import * as fs from "fs";
54
import * as path from "path";
65
import * as Codec from "tiny-decoders";
@@ -68,7 +67,11 @@ import {
6867
TargetName,
6968
WebSocketToken,
7069
} from "./Types";
71-
import { WebSocketServer, WebSocketServerMsg } from "./WebSocketServer";
70+
import {
71+
WebSocketConnectionRejectedReason,
72+
WebSocketServer,
73+
WebSocketServerMsg,
74+
} from "./WebSocketServer";
7275

7376
type WatcherEventName = "added" | "changed" | "removed";
7477

@@ -111,6 +114,12 @@ type WebSocketRelatedEvent =
111114
tag: "WebSocketConnectedWithErrors";
112115
date: Date;
113116
}
117+
| {
118+
tag: "WebSocketConnectionRejected";
119+
date: Date;
120+
origin: string | undefined;
121+
reason: WebSocketConnectionRejectedReason;
122+
}
114123
| {
115124
tag: "WorkersLimitedAfterWebSocketClosed";
116125
date: Date;
@@ -190,6 +199,12 @@ type Msg =
190199
webSocket: WebSocket;
191200
parseWebSocketConnectRequestUrlResult: ParseWebSocketConnectRequestUrlResult;
192201
}
202+
| {
203+
tag: "WebSocketConnectionRejected";
204+
date: Date;
205+
origin: string | undefined;
206+
reason: WebSocketConnectionRejectedReason;
207+
}
193208
| {
194209
tag: "WebSocketMessageReceived";
195210
date: Date;
@@ -516,7 +531,7 @@ const initMutable =
516531
);
517532

518533
const {
519-
webSocketServer = new WebSocketServer(portChoice),
534+
webSocketServer = new WebSocketServer(portChoice, webSocketToken),
520535
webSocketConnections = [],
521536
} = webSocketState ?? {};
522537

@@ -540,7 +555,6 @@ const initMutable =
540555
getNow(),
541556
logger,
542557
mutable,
543-
webSocketToken,
544558
dispatch,
545559
resolvePromise,
546560
rejectPromise,
@@ -1054,6 +1068,23 @@ function update(
10541068
}
10551069
}
10561070

1071+
case "WebSocketConnectionRejected":
1072+
return [
1073+
{
1074+
...model,
1075+
latestEvents: [
1076+
...model.latestEvents,
1077+
{
1078+
tag: "WebSocketConnectionRejected",
1079+
date: msg.date,
1080+
origin: msg.origin,
1081+
reason: msg.reason,
1082+
},
1083+
],
1084+
},
1085+
[],
1086+
];
1087+
10571088
case "WebSocketMessageReceived": {
10581089
const result = parseWebSocketToServerMessage(msg.data);
10591090

@@ -1788,7 +1819,6 @@ function onWebSocketServerMsg(
17881819
now: Date,
17891820
logger: Logger,
17901821
mutable: Mutable,
1791-
webSocketToken: WebSocketToken,
17921822
dispatch: (msg: Msg) => void,
17931823
resolvePromise: (result: HotRunResult) => void,
17941824
rejectPromise: (error: Error) => void,
@@ -1798,8 +1828,7 @@ function onWebSocketServerMsg(
17981828
case "WebSocketConnected": {
17991829
const result = parseWebSocketConnectRequestUrl(
18001830
mutable.project,
1801-
webSocketToken,
1802-
msg.urlString,
1831+
msg.urlParams,
18031832
);
18041833
const webSocketConnection: WebSocketConnection = {
18051834
webSocket: msg.webSocket,
@@ -1816,6 +1845,15 @@ function onWebSocketServerMsg(
18161845
return;
18171846
}
18181847

1848+
case "WebSocketConnectionRejected":
1849+
dispatch({
1850+
tag: "WebSocketConnectionRejected",
1851+
date: now,
1852+
origin: msg.origin,
1853+
reason: msg.reason,
1854+
});
1855+
return;
1856+
18191857
case "WebSocketClosed": {
18201858
const removedConnection = mutable.webSocketConnections.find(
18211859
(connection) => connection.webSocket === msg.webSocket,
@@ -2187,15 +2225,10 @@ type ParseWebSocketConnectRequestUrlResult =
21872225
};
21882226

21892227
type ParseWebSocketConnectRequestUrlError =
2190-
| {
2191-
tag: "BadUrl";
2192-
expectedStart: typeof WEBSOCKET_URL_EXPECTED_START;
2193-
actualUrlString: string;
2194-
}
21952228
| {
21962229
tag: "ParamsDecodeError";
21972230
error: Codec.DecoderError;
2198-
actualUrlString: string;
2231+
urlParams: URLSearchParams;
21992232
}
22002233
| {
22012234
tag: "TargetDisabled";
@@ -2209,60 +2242,28 @@ type ParseWebSocketConnectRequestUrlError =
22092242
enabledOutputs: Array<OutputPath>;
22102243
disabledOutputs: Array<OutputPath>;
22112244
}
2212-
| {
2213-
tag: "WrongToken";
2214-
}
22152245
| {
22162246
tag: "WrongVersion";
22172247
expectedVersion: "%VERSION%";
22182248
actualVersion: string;
22192249
};
22202250

2221-
// We used to require `/?`. Putting “elm-watch” in the path is useful for people
2222-
// running elm-watch behind a proxy: They can use the same port for both the web
2223-
// site and elm-watch, and direct traffic by path matching.
2224-
const WEBSOCKET_URL_EXPECTED_START = "/elm-watch?";
2225-
22262251
function parseWebSocketConnectRequestUrl(
22272252
project: Project,
2228-
webSocketToken: WebSocketToken,
2229-
urlString: string,
2253+
urlParams: URLSearchParams,
22302254
): ParseWebSocketConnectRequestUrlResult {
2231-
if (!urlString.startsWith(WEBSOCKET_URL_EXPECTED_START)) {
2232-
return {
2233-
tag: "BadUrl",
2234-
expectedStart: WEBSOCKET_URL_EXPECTED_START,
2235-
actualUrlString: urlString,
2236-
};
2237-
}
2238-
2239-
// This never throws as far as I can tell.
2240-
const params = new URLSearchParams(
2241-
urlString.slice(WEBSOCKET_URL_EXPECTED_START.length),
2242-
);
2243-
22442255
const webSocketConnectedParamsResult = WebSocketConnectedParams.decoder(
2245-
Object.fromEntries(params),
2256+
Object.fromEntries(urlParams),
22462257
);
22472258
if (webSocketConnectedParamsResult.tag === "DecoderError") {
22482259
return {
22492260
tag: "ParamsDecodeError",
22502261
error: webSocketConnectedParamsResult.error,
2251-
actualUrlString: urlString,
2262+
urlParams,
22522263
};
22532264
}
22542265
const webSocketConnectedParams = webSocketConnectedParamsResult.value;
22552266

2256-
const actualToken = Buffer.from(webSocketConnectedParams.webSocketToken);
2257-
const expectedToken = Buffer.from(webSocketToken);
2258-
const tokenIsCorrect =
2259-
Buffer.byteLength(actualToken) === Buffer.byteLength(expectedToken) &&
2260-
crypto.timingSafeEqual(actualToken, expectedToken);
2261-
2262-
if (!tokenIsCorrect) {
2263-
return { tag: "WrongToken" };
2264-
}
2265-
22662267
if (webSocketConnectedParams.elmWatchVersion !== "%VERSION%") {
22672268
return {
22682269
tag: "WrongVersion",
@@ -2340,17 +2341,8 @@ function webSocketConnectRequestUrlErrorToString(
23402341
error: ParseWebSocketConnectRequestUrlError,
23412342
): string {
23422343
switch (error.tag) {
2343-
case "BadUrl":
2344-
return Errors.webSocketBadUrl(error.expectedStart, error.actualUrlString);
2345-
23462344
case "ParamsDecodeError":
2347-
return Errors.webSocketParamsDecodeError(
2348-
error.error,
2349-
error.actualUrlString,
2350-
);
2351-
2352-
case "WrongToken":
2353-
return Errors.webSocketWrongToken();
2345+
return Errors.webSocketParamsDecodeError(error.error, error.urlParams);
23542346

23552347
case "WrongVersion":
23562348
return Errors.webSocketWrongVersion(
@@ -2763,6 +2755,7 @@ function getLatestEventSleepMs(event: LatestEvent): number {
27632755
case "WebSocketConnectedNeedingCompilation":
27642756
case "WebSocketConnectedNeedingNoAction":
27652757
case "WebSocketConnectedWithErrors":
2758+
case "WebSocketConnectionRejected":
27662759
case "WorkersLimitedAfterWebSocketClosed":
27672760
return 100;
27682761

@@ -2923,6 +2916,12 @@ function printEventMessage(event: LatestEvent): string {
29232916
case "WebSocketConnectedWithErrors":
29242917
return `Web socket connected with errors (see the browser for details)`;
29252918

2919+
case "WebSocketConnectionRejected": {
2920+
const origin =
2921+
event.origin === undefined ? "unknown origin" : quote(event.origin);
2922+
return `Web socket connection from ${origin} rejected due to: ${printWebSocketConnectionRejectedReason(event.reason)}`;
2923+
}
2924+
29262925
case "WebSocketChangedBrowserUiPosition":
29272926
return `Changed browser UI position to ${quote(
29282927
event.browserUiPosition,
@@ -2965,3 +2964,14 @@ function printEventsMessage(
29652964
? `FYI: The above Elm ${what1} not imported by ${what2}. Nothing to do!`
29662965
: "Everything up to date.";
29672966
}
2967+
2968+
function printWebSocketConnectionRejectedReason(
2969+
reason: WebSocketConnectionRejectedReason,
2970+
): string {
2971+
switch (reason.tag) {
2972+
case "BadUrl":
2973+
return `wrong URL prefix – ${quote(reason.expectedStart)} != ${quote(reason.actualUrlString.slice(0, reason.expectedStart.length))}`;
2974+
case "WrongToken":
2975+
return "invalid security token";
2976+
}
2977+
}

0 commit comments

Comments
 (0)