Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot]
7c0a76b096 Bump @eslint/compat from 1.4.1 to 2.1.1
Bumps [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat) from 1.4.1 to 2.1.1.
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/compat-v2.1.1/packages/compat)

---
updated-dependencies:
- dependency-name: "@eslint/compat"
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-27 01:04:06 +00:00
dependabot[bot]
9d0eee156e Bump ip-address from 10.7.2 to 10.7.5 (#8080) 2026-10-26 17:01:48 -08:00
dependabot[bot]
b87ae88bc6 Bump body-parser from 2.3.0 to 2.4.0 (#8079) 2026-10-26 17:01:24 -08:00
Harsh Bhanushali
89cba83cb4 Normalize default ports and use X-Forwarded-Port in the origin check (#8059)
The origin check compared the host header to the origin's host as plain
strings, so `example.com:443` did not match `https://example.com`, and a
proxy forwarding `$host` (no port) together with X-Forwarded-Port failed
on any non-default port.

Default ports (80 for http, 443 for https) are now treated as no port,
and when X-Forwarded-Host has no port the port from X-Forwarded-Port is
used.  Everything else is still an exact match.  getHost() is unchanged
for the domain proxy.
2026-10-26 16:42:24 -08:00
dependabot[bot]
b45f29491d Bump js-yaml from 4.3.1 to 5.4.3 (#8054) 2026-10-26 15:47:26 -08:00
cdrci
4677c9d865 Update Helm chart and changelog with 4.143.1 (#8072) 2026-10-23 15:32:11 -08:00
7 changed files with 213 additions and 41 deletions

View File

@@ -22,6 +22,8 @@ Code v99.99.999
## Unreleased
## [4.143.1](https://github.com/coder/code-server/releases/tag/v4.143.1) - 2026-10-23
Code v1.143.1
### Changed

View File

@@ -15,9 +15,9 @@ type: application
# 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.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 3.56.1
version: 3.57.0
# 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
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 4.141.1
appVersion: 4.143.1

View File

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

75
package-lock.json generated
View File

@@ -35,7 +35,7 @@
"code-server": "out/node/entry.js"
},
"devDependencies": {
"@eslint/compat": "^1.2.0",
"@eslint/compat": "^2.1.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.12.0",
"@types/compression": "^1.7.3",
@@ -170,19 +170,19 @@
}
},
"node_modules/@eslint/compat": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
"integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.1.1.tgz",
"integrity": "sha512-rMcy8GSrwNzcISX/BlTDY/GLB4eCopEuy9woIls3To+15OLxykZrxxq+WUcylCPCQ6F4MujjBM1DX5V1aqI3Vw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
"@eslint/core": "^1.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"peerDependencies": {
"eslint": "^8.40 || 9"
"eslint": "^8.40 || 9 || 10"
},
"peerDependenciesMeta": {
"eslint": {
@@ -190,6 +190,19 @@
}
}
},
"node_modules/@eslint/compat/node_modules/@eslint/core": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/config-array": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
@@ -269,9 +282,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -1735,18 +1748,18 @@
}
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
"integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^2.0.0",
"content-type": "^2.1.0",
"debug": "^4.4.3",
"http-errors": "^2.0.1",
"iconv-lite": "^0.7.2",
"iconv-lite": "^0.7.3",
"on-finished": "^2.4.1",
"qs": "^6.15.2",
"qs": "^6.16.1",
"raw-body": "^3.0.2",
"type-is": "^2.1.0"
},
@@ -1759,9 +1772,9 @@
}
},
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -3703,9 +3716,9 @@
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -3777,9 +3790,9 @@
}
},
"node_modules/ip-address": {
"version": "10.7.2",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz",
"integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==",
"version": "10.7.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -4270,9 +4283,9 @@
"license": "ISC"
},
"node_modules/js-yaml": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz",
"integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==",
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz",
"integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==",
"funding": [
{
"type": "github",
@@ -5366,9 +5379,9 @@
}
},
"node_modules/qs": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz",
"integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",

View File

@@ -36,7 +36,7 @@
},
"main": "out/node/entry.js",
"devDependencies": {
"@eslint/compat": "^1.2.0",
"@eslint/compat": "^2.1.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.12.0",
"@types/compression": "^1.7.3",

View File

@@ -381,19 +381,20 @@ export function authenticateOrigin(req: express.Request): void {
return
}
let origin: string
let originUrl: URL
try {
origin = new URL(originRaw).host.trim().toLowerCase()
originUrl = new URL(originRaw)
} catch (error) {
throw new Error(`unable to parse malformed origin "${originRaw}"`)
}
const origin = originUrl.host.trim().toLowerCase()
const trustedOrigins = req.args["trusted-origins"] || []
if (isTrustedOrigin(origin, trustedOrigins)) {
return
}
const host = getHost(req)
const host = getHost(req, true)
if (typeof host === "undefined") {
// A missing host likely means the reverse proxy has not been configured to
// forward the host which means we cannot perform the check. Emit an error
@@ -403,15 +404,64 @@ export function authenticateOrigin(req: express.Request): void {
throw new Error("no host headers found")
}
if (host !== origin) {
if (!hostMatchesOrigin(host, originUrl)) {
throw new Error(`host "${host}" does not match origin "${origin}"`)
}
}
const defaultPorts: { [protocol: string]: string } = {
"http:": "80",
"https:": "443",
}
/**
* Parse a host like `example.com:8080` or `[::1]:8080`. A non-special scheme
* keeps the port as written instead of dropping default ports.
*/
function parseHost(host: string): URL | undefined {
try {
return new URL(`host://${host}`)
} catch (error) {
return undefined
}
}
/**
* Check whether the host from the request headers matches the origin.
*
* This is an exact match, except that a default port (80 for http, 443 for
* https) is the same as no port, since `example.com:443` and
* `https://example.com` are the same origin.
*/
export function hostMatchesOrigin(host: string, origin: URL): boolean {
const parsed = parseHost(host)
if (!parsed) {
return false
}
const port = parsed.port === defaultPorts[origin.protocol] ? "" : parsed.port
return parsed.hostname === origin.hostname.toLowerCase() && port === origin.port
}
/**
* Add the port from X-Forwarded-Port to a host that does not have one. Some
* reverse proxies (for example nginx with `$host`) forward the host without
* the port and send the port separately.
*/
function addForwardedPort(req: express.Request, host: string): string {
const port = getFirstHeader(req, "x-forwarded-port")?.split(",")[0].trim()
if (!port || !/^\d+$/.test(port) || parseHost(host)?.port !== "") {
return host
}
return `${host}:${port}`
}
/**
* Get the host from headers. It will be trimmed and lowercased.
*
* If `withForwardedPort` is set and the host comes from X-Forwarded-Host
* without a port, the port from X-Forwarded-Port is added.
*/
export function getHost(req: express.Request): string | undefined {
export function getHost(req: express.Request, withForwardedPort = false): string | undefined {
// Honor Forwarded if present.
const forwardedRaw = getFirstHeader(req, "forwarded")
if (forwardedRaw) {
@@ -430,7 +480,8 @@ export function getHost(req: express.Request): string | undefined {
if (xHost) {
const firstXHost = xHost.split(",")[0]
if (firstXHost) {
return firstXHost.trim().toLowerCase()
const host = firstXHost.trim().toLowerCase()
return withForwardedPort ? addForwardedPort(req, host) : host
}
}

View File

@@ -68,6 +68,43 @@ describe("http", () => {
host: "localhost:8081",
expected: "does not match",
},
{
origin: "https://example.com",
host: "example.com:443", // Explicit default port.
},
{
origin: "http://example.com",
host: "example.com:80",
},
{
origin: "https://example.com",
host: "example.com:80", // Default port of the other protocol.
expected: "does not match",
},
{
origin: "https://example.com:8080",
host: "example.com", // Missing port is not ignored.
expected: "does not match",
},
{
origin: "https://example.com",
host: "example.com:8443",
expected: "does not match",
},
{
origin: "http://example.com:8080",
host: "other.com",
expected: "does not match",
},
{
origin: "http://[::1]:8080",
host: "[::1]:8080",
},
{
origin: "http://[::1]:8080",
host: "[::1]:8081",
expected: "does not match",
},
{
origin: "localhost:8080",
host: "localhost:8080",
@@ -122,6 +159,75 @@ describe("http", () => {
})
})
})
describe("x-forwarded-port", () => {
;[
{
origin: "https://example.com:8080",
headers: { "x-forwarded-host": "example.com", "x-forwarded-port": "8080" },
},
{
origin: "https://example.com",
headers: { "x-forwarded-host": "example.com", "x-forwarded-port": "443" },
},
{
origin: "http://[::1]:8080",
headers: { "x-forwarded-host": "[::1]", "x-forwarded-port": "8080" },
},
{
origin: "https://example.com:8080",
headers: { "x-forwarded-host": "example.com", "x-forwarded-port": "8080, 443" },
},
{
origin: "https://example.com:8080",
headers: { "x-forwarded-host": "example.com", "x-forwarded-port": "8081" },
expected: "does not match",
},
{
origin: "https://example.com:8080",
headers: { "x-forwarded-host": "example.com:8081", "x-forwarded-port": "8080" },
expected: "does not match", // The port in the host wins.
},
{
origin: "https://example.com:8080",
headers: { "x-forwarded-host": "example.com", "x-forwarded-port": "foo" },
expected: "does not match",
},
{
origin: "https://example.com:8080",
headers: { host: "example.com", "x-forwarded-port": "8080" },
expected: "does not match", // Only used with X-Forwarded-Host.
},
{
origin: "https://example.com:8080",
headers: { forwarded: "host=example.com", "x-forwarded-port": "8080" },
expected: "does not match",
},
].forEach((test) => {
it(`${test.origin} -> ${JSON.stringify(test.headers)}`, () => {
const req = getMockReq({
originalUrl: "localhost:8080",
headers: {
origin: test.origin,
...test.headers,
},
args: {},
})
if (typeof test.expected === "string") {
expect(() => http.authenticateOrigin(req)).toThrow(test.expected)
} else {
expect(() => http.authenticateOrigin(req)).not.toThrow()
}
})
})
it("should not add the port to the host for other uses", () => {
const req = getMockReq({
headers: { "x-forwarded-host": "code-8080.example.com", "x-forwarded-port": "443" },
})
expect(http.getHost(req)).toBe("code-8080.example.com")
})
})
})
describe("constructRedirectPath", () => {