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/drivers/shared/nginx-vhost.tsmodified+18-1
Changes to packages/ts-cloud/src/drivers/shared/nginx-vhost.ts
@@ -269,7 +269,24 @@ function vhostBody(options: NginxVhostOptions): string[] {
269269 * otherwise a single :80 block (certbot upgrades it for Let's Encrypt).
270270 */
271271export function buildNginxVhost(options: NginxVhostOptions): string {
272 const serverNames = [options.domain, ...(options.aliases || [])].filter(Boolean).join(' ')
272 const hosts = [options.domain, ...(options.aliases || [])].filter(Boolean) as string[]
273 // Defense in depth: every token here is interpolated straight into a
274 // `server_name` directive, and nginx is whitespace-insensitive — so anything
275 // that isn't a bare hostname could close this block and open another. Callers
276 // validate too; refuse here as well so no future path can slip a directive in.
277 //
278 // Deliberately laxer than `isValidHostname` (which the dashboard API uses for
279 // user-supplied domains and which requires a dot): a single label is valid
280 // here because compute-deploy falls back to `site.domain || siteName`, so an
281 // internal site legitimately arrives as `main` or `docs`. What matters for
282 // safety is only that a token can't contain whitespace, `;`, `{` or `}`.
283 for (const host of hosts) {
284 if (!/^(?=.{1,253}$)(?:\*\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(host.trim()))
285 throw new Error(`Refusing to build a vhost: '${host}' is not a valid hostname.`)
286 }
287 if (!hosts.length)
288 throw new Error('Refusing to build a vhost: no server_name (domain) was given.')
289 const serverNames = hosts.join(' ')
273290 const body = vhostBody(options)
274291
275292 if (options.ssl) {