Skip to content

Commit 56fa989

Browse files
committed
Strip token from cookies before proxying
Since this functionality requires information placed onto the request by code-server (req.args) and Express (req.cookies), move the standalone tests into the integration tests as the proxy can no longer run correctly on its own without that context. We could strip the header elsewhere or refactor in some way (pass in a callback function for the stripping or something) but this seems like the simplest and safest place at the moment to ensure we catch all uses of the proxy. In any case, I think it does lend more confidence to know we are testing the proxy the way it will be used in practice. The downside is some additional complexity when setting up tests, but at the moment I do not think that exchange is overly burdensome.
1 parent 92a7dce commit 56fa989

4 files changed

Lines changed: 78 additions & 67 deletions

File tree

package-lock.json

Lines changed: 27 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@coder/logger": "^3.0.1",
7171
"argon2": "^0.44.0",
7272
"compression": "^1.7.4",
73+
"cookie": "^1.1.1",
7374
"cookie-parser": "^1.4.6",
7475
"env-paths": "^2.2.1",
7576
"express": "^5.0.1",

src/node/proxy.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import * as cookie from "cookie"
2+
import type { Request } from "express"
13
import proxyServer from "http-proxy"
2-
import { HttpCode } from "../common/http"
4+
import { getCookieSessionName, HttpCode } from "../common/http"
35

46
export const proxy = proxyServer.createProxyServer({})
57

@@ -18,6 +20,16 @@ proxy.on("error", (error, _, res) => {
1820
}
1921
})
2022

23+
// Strip the code-server cookie if it exists to avoid transmitting the cookie
24+
// to potentially malicious local ports.
25+
proxy.on("proxyReq", (preq, req) => {
26+
const cookieSessionName = getCookieSessionName((req as Request).args["cookie-suffix"])
27+
preq.setHeader("Cookie", cookie.stringifyCookie({
28+
...(req as Request).cookies,
29+
[cookieSessionName]: undefined,
30+
}))
31+
})
32+
2133
// Intercept the response to rewrite absolute redirects against the base path.
2234
// Is disabled when the request has no base path which means /absproxy is in use.
2335
proxy.on("proxyRes", (res, req) => {

test/unit/node/proxy.test.ts

Lines changed: 37 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
import * as express from "express"
2-
import * as http from "http"
3-
import nodeFetch from "node-fetch"
42
import { HttpCode } from "../../../src/common/http"
5-
import { proxy } from "../../../src/node/proxy"
63
import { wss, Router as WsRouter } from "../../../src/node/wsRouter"
7-
import { getAvailablePort, mockLogger } from "../../utils/helpers"
4+
import { mockLogger } from "../../utils/helpers"
85
import * as httpserver from "../../utils/httpserver"
96
import * as integration from "../../utils/integration"
107

118
describe("proxy", () => {
12-
const nhooyrDevServer = new httpserver.HttpServer()
9+
const proxyTarget = new httpserver.HttpServer()
1310
const wsApp = express.default()
1411
const wsRouter = WsRouter()
1512
let codeServer: httpserver.HttpServer | undefined
@@ -19,21 +16,22 @@ describe("proxy", () => {
1916

2017
beforeAll(async () => {
2118
wsApp.use("/", wsRouter.router)
22-
await nhooyrDevServer.listen((req, res) => {
19+
await proxyTarget.listen((req, res) => {
2320
e(req, res)
2421
})
25-
nhooyrDevServer.listenUpgrade(wsApp)
26-
proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup`
22+
proxyTarget.listenUpgrade(wsApp)
23+
proxyPath = `/proxy/${proxyTarget.port()}/wsup`
2724
absProxyPath = proxyPath.replace("/proxy/", "/absproxy/")
2825
})
2926

3027
afterAll(async () => {
31-
await nhooyrDevServer.dispose()
28+
await proxyTarget.dispose()
3229
})
3330

3431
beforeEach(() => {
3532
e = express.default()
3633
mockLogger()
34+
delete process.env.PASSWORD
3735
})
3836

3937
afterEach(async () => {
@@ -283,65 +281,42 @@ describe("proxy", () => {
283281
const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" })
284282
expect(resp.status).toBe(200)
285283
})
286-
})
287284

288-
// NOTE@jsjoeio
289-
// Both this test suite and the one above it are very similar
290-
// The main difference is this one uses http and node-fetch
291-
// and specifically tests the proxy in isolation vs. using
292-
// the httpserver abstraction we've built.
293-
//
294-
// Leaving this as a separate test suite for now because
295-
// we may consider refactoring the httpserver abstraction
296-
// in the future.
297-
//
298-
// If you're writing a test specifically for code in
299-
// src/node/proxy.ts, you should probably add it to
300-
// this test suite.
301-
describe("proxy (standalone)", () => {
302-
let URL = ""
303-
let PROXY_URL = ""
304-
let testServer: http.Server
305-
let proxyTarget: http.Server
285+
it("should return a 500 when no target is running ", async () => {
286+
const target = new httpserver.HttpServer()
287+
await target.listen(() => {})
288+
const port = target.port()
289+
target.dispose()
290+
codeServer = await integration.setup(["--auth=none"], "")
291+
const resp = await codeServer.fetch(`/proxy/${port}/wsup`)
292+
expect(resp.status).toBe(HttpCode.ServerError)
293+
expect(resp.statusText).toBe("Internal Server Error")
294+
})
306295

307-
beforeEach(async () => {
308-
const PORT = await getAvailablePort()
309-
const PROXY_PORT = await getAvailablePort()
310-
URL = `http://localhost:${PORT}`
311-
PROXY_URL = `http://localhost:${PROXY_PORT}`
312-
// Define server and a proxy server
313-
testServer = http.createServer((req, res) => {
314-
proxy.web(req, res, {
315-
target: PROXY_URL,
316-
})
317-
})
296+
it("should strip token cookie", async () => {
297+
const token = "my-super-secure-token"
298+
process.env.HASHED_PASSWORD = token
299+
codeServer = await integration.setup(["--auth=password"])
318300

319-
proxyTarget = http.createServer((req, res) => {
320-
res.writeHead(200, { "Content-Type": "text/plain" })
321-
res.end()
301+
// Set up a listener that just prints the cookies it got.
302+
e.get("/wsup/cookies", (req, res) => {
303+
res.writeHead(HttpCode.Ok, { "Content-Type": "text/plain" })
304+
res.end(req.headers.cookie)
322305
})
323306

324-
// Start both servers
325-
proxyTarget.listen(PROXY_PORT)
326-
testServer.listen(PORT)
327-
})
328-
329-
afterEach(async () => {
330-
testServer.close()
331-
proxyTarget.close()
332-
})
333-
334-
it("should return a 500 when proxy target errors ", async () => {
335-
// Close the proxy target so that proxy errors
336-
proxyTarget.close()
337-
const errorResp = await nodeFetch(`${URL}/error`)
338-
expect(errorResp.status).toBe(HttpCode.ServerError)
339-
expect(errorResp.statusText).toBe("Internal Server Error")
340-
})
307+
// Send the token along with other cookies which should be preserved.
308+
// Encode one to make sure they are being re-encoded properly.
309+
const value = "hello=there"
310+
const encodedValue = encodeURIComponent(value)
311+
const resp = await codeServer.fetch(proxyPath + "/cookies", {
312+
headers: {
313+
cookie: `cookie1=${encodedValue}; code-server-session=${token}; cookie2=hello;`,
314+
},
315+
})
341316

342-
it("should proxy correctly", async () => {
343-
const resp = await nodeFetch(`${URL}/route`)
317+
// The proxied listener should not have printed the code-server token.
344318
expect(resp.status).toBe(200)
345-
expect(resp.statusText).toBe("OK")
319+
const text = await resp.text()
320+
expect(text).toBe(`cookie1=${encodedValue}; cookie2=hello`)
346321
})
347322
})

0 commit comments

Comments
 (0)