Compare commits

..

5 Commits

Author SHA1 Message Date
cdrci
a8c85f47c3 Update Code to 1.124.2 2026-06-15 16:45:41 -08:00
Asher
1ccd4f04d2 Update brace-expansion, js-yaml, and ws 2026-06-15 16:42:17 -08:00
Asher
364cf99338 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.
2026-06-15 16:42:17 -08:00
ka-ishimoto
92a7dce46f Fix false positive CVE alerts by setting package name to code-oss-dev (#7839)
The VS Code build process sets the bundled lib/vscode/package.json name to "code-server" (from product.json nameShort), causing vulnerability scanners to misidentify it and flag non-applicable CVEs. Override the name to "code-oss-dev" in build-release.sh after merging package.json.
Fixes #7071

Signed-off-by: ka-ishimoto <ka-ishimoto@kddi.com>
2026-06-10 11:59:45 -08:00
cdrci
d0d53d924e Update Helm chart and changelog with 4.123.0 (#7838) 2026-06-04 10:50:04 -08:00
12 changed files with 127 additions and 85 deletions

View File

@@ -144,7 +144,7 @@ jobs:
test/package-lock.json test/package-lock.json
- run: SKIP_SUBMODULE_DEPS=1 npm ci - run: SKIP_SUBMODULE_DEPS=1 npm ci
- run: npm run test:unit - run: npm run test:unit
- uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 - uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
if: success() if: success()
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -22,6 +22,22 @@ Code v99.99.999
## Unreleased ## Unreleased
Code v1.124.2
### Security
- Strip code-server's session token from the cookie before proxying to a local
port. Previously, when you used built-in password authentication, the cookie
would be sent to the local proxied port, which meant if the service was
malicious and not already running as your code-server user it could use the
cookie to log into code-server and execute commands as your code-server user.
### Changed
- Update to Code 1.124.2
## [4.123.0](https://github.com/coder/code-server/releases/tag/v4.123.0) - 2026-06-03
Code v1.123.0 Code v1.123.0
### Changed ### Changed

View File

@@ -128,7 +128,9 @@ bundle_vscode() {
# Merge the package.json for the web/remote server so we can include # Merge the package.json for the web/remote server so we can include
# dependencies, since we want to ship this via NPM. # dependencies, since we want to ship this via NPM.
jq --slurp '.[0] * .[1]' \ # Also override the name to prevent vulnerability scanners from
# misidentifying this package as VS Code (see #7071).
jq --slurp '.[0] * .[1] | .name = "code-oss-dev"' \
"$VSCODE_SRC_PATH/remote/package.json" \ "$VSCODE_SRC_PATH/remote/package.json" \
"$VSCODE_OUT_PATH/package.json" > "$VSCODE_OUT_PATH/package.json.merged" "$VSCODE_OUT_PATH/package.json" > "$VSCODE_OUT_PATH/package.json.merged"
mv "$VSCODE_OUT_PATH/package.json.merged" "$VSCODE_OUT_PATH/package.json" mv "$VSCODE_OUT_PATH/package.json.merged" "$VSCODE_OUT_PATH/package.json"

View File

@@ -15,9 +15,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 3.37.1 version: 3.38.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 4.122.1 appVersion: 4.123.0

View File

@@ -6,7 +6,7 @@ replicaCount: 1
image: image:
repository: codercom/code-server repository: codercom/code-server
tag: '4.122.1' tag: '4.123.0'
pullPolicy: Always pullPolicy: Always
# Specifies one or more secrets to be used when pulling images from a # Specifies one or more secrets to be used when pulling images from a

59
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@coder/logger": "^3.0.1", "@coder/logger": "^3.0.1",
"argon2": "^0.44.0", "argon2": "^0.44.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie": "^1.1.1",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"env-paths": "^2.2.1", "env-paths": "^2.2.1",
"express": "^5.0.1", "express": "^5.0.1",
@@ -968,9 +969,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.5", "version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1936,12 +1937,16 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/cookie-parser": { "node_modules/cookie-parser": {
@@ -1957,6 +1962,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -2953,6 +2967,15 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/cookie-signature": { "node_modules/express/node_modules/cookie-signature": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -4130,9 +4153,19 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -6613,9 +6646,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.20.1", "version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@@ -70,6 +70,7 @@
"@coder/logger": "^3.0.1", "@coder/logger": "^3.0.1",
"argon2": "^0.44.0", "argon2": "^0.44.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie": "^1.1.1",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"env-paths": "^2.2.1", "env-paths": "^2.2.1",
"express": "^5.0.1", "express": "^5.0.1",

View File

@@ -7,7 +7,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts --- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
+++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts
@@ -344,6 +344,10 @@ export class Extension implements IExten @@ -345,6 +345,10 @@ export class Extension implements IExten
if (this.type === ExtensionType.System && this.productService.quality === 'stable' && !this.productService.builtInExtensionsEnabledWithAutoUpdates?.some(id => id.toLowerCase() === this.identifier.id.toLowerCase())) { if (this.type === ExtensionType.System && this.productService.quality === 'stable' && !this.productService.builtInExtensionsEnabledWithAutoUpdates?.some(id => id.toLowerCase() === this.identifier.id.toLowerCase())) {
return false; return false;
} }

View File

@@ -70,8 +70,8 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/webview/browser/pre/index
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
- content="default-src 'none'; script-src 'sha256-q+WTr+fBXpLLE3++yWNaxT6BTWQtsKscoeIlynBRk4E=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> - content="default-src 'none'; script-src 'sha256-nXjtuhBilO++r8hfxl5VjEScSmdm07wDAk6jw228DgM=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
+ content="default-src 'none'; script-src 'sha256-m1DlJtsIJd46QuWYNcsaYIG1xI+9FyjKQu+cfp+zq5Q=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> + content="default-src 'none'; script-src 'sha256-A6/szVNdTzyi4hDa+9OLbzS8tSd2iUV4CqimLNWex2Y=' 'self'; frame-src 'self'; style-src 'unsafe-inline';">
<!-- Disable pinch zooming --> <!-- Disable pinch zooming -->
<meta name="viewport" <meta name="viewport"

View File

@@ -1,5 +1,7 @@
import * as cookie from "cookie"
import type { Request } from "express"
import proxyServer from "http-proxy" import proxyServer from "http-proxy"
import { HttpCode } from "../common/http" import { getCookieSessionName, HttpCode } from "../common/http"
export const proxy = proxyServer.createProxyServer({}) export const proxy = proxyServer.createProxyServer({})
@@ -18,6 +20,19 @@ proxy.on("error", (error, _, res) => {
} }
}) })
// Strip the code-server cookie if it exists to avoid transmitting the cookie
// to potentially malicious local ports.
proxy.on("proxyReq", (preq, req) => {
const cookieSessionName = getCookieSessionName((req as Request).args["cookie-suffix"])
preq.setHeader(
"Cookie",
cookie.stringifyCookie({
...(req as Request).cookies,
[cookieSessionName]: undefined,
}),
)
})
// Intercept the response to rewrite absolute redirects against the base path. // Intercept the response to rewrite absolute redirects against the base path.
// Is disabled when the request has no base path which means /absproxy is in use. // Is disabled when the request has no base path which means /absproxy is in use.
proxy.on("proxyRes", (res, req) => { proxy.on("proxyRes", (res, req) => {

View File

@@ -1,15 +1,12 @@
import * as express from "express" import * as express from "express"
import * as http from "http"
import nodeFetch from "node-fetch"
import { HttpCode } from "../../../src/common/http" import { HttpCode } from "../../../src/common/http"
import { proxy } from "../../../src/node/proxy"
import { wss, Router as WsRouter } from "../../../src/node/wsRouter" import { wss, Router as WsRouter } from "../../../src/node/wsRouter"
import { getAvailablePort, mockLogger } from "../../utils/helpers" import { mockLogger } from "../../utils/helpers"
import * as httpserver from "../../utils/httpserver" import * as httpserver from "../../utils/httpserver"
import * as integration from "../../utils/integration" import * as integration from "../../utils/integration"
describe("proxy", () => { describe("proxy", () => {
const nhooyrDevServer = new httpserver.HttpServer() const proxyTarget = new httpserver.HttpServer()
const wsApp = express.default() const wsApp = express.default()
const wsRouter = WsRouter() const wsRouter = WsRouter()
let codeServer: httpserver.HttpServer | undefined let codeServer: httpserver.HttpServer | undefined
@@ -19,21 +16,22 @@ describe("proxy", () => {
beforeAll(async () => { beforeAll(async () => {
wsApp.use("/", wsRouter.router) wsApp.use("/", wsRouter.router)
await nhooyrDevServer.listen((req, res) => { await proxyTarget.listen((req, res) => {
e(req, res) e(req, res)
}) })
nhooyrDevServer.listenUpgrade(wsApp) proxyTarget.listenUpgrade(wsApp)
proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup` proxyPath = `/proxy/${proxyTarget.port()}/wsup`
absProxyPath = proxyPath.replace("/proxy/", "/absproxy/") absProxyPath = proxyPath.replace("/proxy/", "/absproxy/")
}) })
afterAll(async () => { afterAll(async () => {
await nhooyrDevServer.dispose() await proxyTarget.dispose()
}) })
beforeEach(() => { beforeEach(() => {
e = express.default() e = express.default()
mockLogger() mockLogger()
delete process.env.PASSWORD
}) })
afterEach(async () => { afterEach(async () => {
@@ -283,65 +281,42 @@ describe("proxy", () => {
const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" }) const resp = await codeServer.fetch(proxyPath, { method: "OPTIONS" })
expect(resp.status).toBe(200) expect(resp.status).toBe(200)
}) })
})
// NOTE@jsjoeio it("should return a 500 when no target is running ", async () => {
// Both this test suite and the one above it are very similar const target = new httpserver.HttpServer()
// The main difference is this one uses http and node-fetch await target.listen(() => {})
// and specifically tests the proxy in isolation vs. using const port = target.port()
// the httpserver abstraction we've built. target.dispose()
// codeServer = await integration.setup(["--auth=none"], "")
// Leaving this as a separate test suite for now because const resp = await codeServer.fetch(`/proxy/${port}/wsup`)
// we may consider refactoring the httpserver abstraction expect(resp.status).toBe(HttpCode.ServerError)
// in the future. expect(resp.statusText).toBe("Internal Server Error")
// })
// If you're writing a test specifically for code in
// src/node/proxy.ts, you should probably add it to
// this test suite.
describe("proxy (standalone)", () => {
let URL = ""
let PROXY_URL = ""
let testServer: http.Server
let proxyTarget: http.Server
beforeEach(async () => { it("should strip token cookie", async () => {
const PORT = await getAvailablePort() const token = "my-super-secure-token"
const PROXY_PORT = await getAvailablePort() process.env.HASHED_PASSWORD = token
URL = `http://localhost:${PORT}` codeServer = await integration.setup(["--auth=password"])
PROXY_URL = `http://localhost:${PROXY_PORT}`
// Define server and a proxy server // Set up a listener that just prints the cookies it got.
testServer = http.createServer((req, res) => { e.get("/wsup/cookies", (req, res) => {
proxy.web(req, res, { res.writeHead(HttpCode.Ok, { "Content-Type": "text/plain" })
target: PROXY_URL, res.end(req.headers.cookie)
})
}) })
proxyTarget = http.createServer((req, res) => { // Send the token along with other cookies which should be preserved.
res.writeHead(200, { "Content-Type": "text/plain" }) // Encode one to make sure they are being re-encoded properly.
res.end() const value = "hello=there"
const encodedValue = encodeURIComponent(value)
const resp = await codeServer.fetch(proxyPath + "/cookies", {
headers: {
cookie: `cookie1=${encodedValue}; code-server-session=${token}; cookie2=hello;`,
},
}) })
// Start both servers // The proxied listener should not have printed the code-server token.
proxyTarget.listen(PROXY_PORT)
testServer.listen(PORT)
})
afterEach(async () => {
testServer.close()
proxyTarget.close()
})
it("should return a 500 when proxy target errors ", async () => {
// Close the proxy target so that proxy errors
proxyTarget.close()
const errorResp = await nodeFetch(`${URL}/error`)
expect(errorResp.status).toBe(HttpCode.ServerError)
expect(errorResp.statusText).toBe("Internal Server Error")
})
it("should proxy correctly", async () => {
const resp = await nodeFetch(`${URL}/route`)
expect(resp.status).toBe(200) expect(resp.status).toBe(200)
expect(resp.statusText).toBe("OK") const text = await resp.text()
expect(text).toBe(`cookie1=${encodedValue}; cookie2=hello`)
}) })
}) })