ReviewOS

also looking at this

stacks/ts-cloud

fix: validate site domain before it reaches nginx server_name

#129
Merged glennmichael123 wants to merge fix/nginx-server-name-injection into main
5 files +143 -5
packages/ts-cloud/src/deploy/local-dashboard-server.tsmodified+10-1
Changes to packages/ts-cloud/src/deploy/local-dashboard-server.ts
@@ -40,7 +40,7 @@ import {
4040 setServerlessSecret,
4141 updateFunctionConfig,
4242} from './serverless-operations'
43import { addSiteToCloudConfig, removeSiteFromCloudConfig, renderAliasesValue, renderEnvValue, renderRedirectsValue, renderSslValue, renderStringValue, setSitePropertyInCloudConfig } from './site-config-editor'
43import { addSiteToCloudConfig, isValidHostname, removeSiteFromCloudConfig, renderAliasesValue, renderEnvValue, renderRedirectsValue, renderSslValue, renderStringValue, setSitePropertyInCloudConfig } from './site-config-editor'
4444import { addSshKeyToCloudConfig, describeSshKeys, removeSshKeyFromCloudConfig } from './ssh-config-editor'
4545import { createTerminalSession } from './terminal-session'
4646
@@ -860,6 +860,15 @@ export async function startLocalDashboardServer(options: LocalDashboardServerOpt
860860 if (body.port !== undefined && body.port !== null && body.port !== '' && (!Number.isInteger(Number(body.port)) || Number(body.port) < 1 || Number(body.port) > 65_535))
861861 return json({ ok: false, error: 'Port must be a number between 1 and 65535.' }, 422)
862862
863 // `domain` lands in the generated nginx `server_name`, so it must be a
864 // hostname and nothing else — an unvalidated value can close the
865 // server block and open an attacker-controlled one. `aliases` (same
866 // destination) has always been checked; this closes the gap for the
867 // primary domain. Validated before any write so a bad value can't
868 // leave the config half-edited.
869 if (typeof body.domain === 'string' && body.domain.trim() && !isValidHostname(body.domain.trim()))
870 return json({ ok: false, error: `Domain '${body.domain.trim()}' is not a valid hostname.` }, 422)
871
863872 let text = await readFile(configPath, 'utf8')
864873 const set = (key: string, valueText: string): void => {
865874 text = setSitePropertyInCloudConfig({ configText: text, siteName: name, key, valueText })