@@ -12,7 +12,15 @@ import { version as codeServerVersion } from "./constants"
12
12
import { Heart } from "./heart"
13
13
import { CoderSettings , SettingsProvider } from "./settings"
14
14
import { UpdateProvider } from "./update"
15
- import { getPasswordMethod , IsCookieValidArgs , isCookieValid , sanitizeString , escapeHtml , escapeJSON } from "./util"
15
+ import {
16
+ getPasswordMethod ,
17
+ IsCookieValidArgs ,
18
+ isCookieValid ,
19
+ sanitizeString ,
20
+ escapeHtml ,
21
+ escapeJSON ,
22
+ splitOnFirstEquals ,
23
+ } from "./util"
16
24
17
25
/**
18
26
* Base options included on every page.
@@ -308,3 +316,68 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
308
316
export const self = ( req : express . Request ) : string => {
309
317
return normalize ( `${ req . baseUrl } ${ req . originalUrl . endsWith ( "/" ) ? "/" : "" } ` , true )
310
318
}
319
+
320
+ function getFirstHeader ( req : http . IncomingMessage , headerName : string ) : string | undefined {
321
+ const val = req . headers [ headerName ]
322
+ return Array . isArray ( val ) ? val [ 0 ] : val
323
+ }
324
+
325
+ /**
326
+ * Throw an error if origin checks fail. Call `next` if provided.
327
+ */
328
+ export function ensureOrigin ( req : express . Request , _ ?: express . Response , next ?: express . NextFunction ) : void {
329
+ if ( ! authenticateOrigin ( req ) ) {
330
+ throw new HttpError ( "Forbidden" , HttpCode . Forbidden )
331
+ }
332
+ if ( next ) {
333
+ next ( )
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Authenticate the request origin against the host.
339
+ */
340
+ export function authenticateOrigin ( req : express . Request ) : boolean {
341
+ // A missing origin probably means the source is non-browser. Not sure we
342
+ // have a use case for this but let it through.
343
+ const originRaw = getFirstHeader ( req , "origin" )
344
+ if ( ! originRaw ) {
345
+ return true
346
+ }
347
+
348
+ let origin : string
349
+ try {
350
+ origin = new URL ( originRaw ) . host . trim ( ) . toLowerCase ( )
351
+ } catch ( error ) {
352
+ return false // Malformed URL.
353
+ }
354
+
355
+ // Honor Forwarded if present.
356
+ const forwardedRaw = getFirstHeader ( req , "forwarded" )
357
+ if ( forwardedRaw ) {
358
+ const parts = forwardedRaw . split ( / [ ; , ] / )
359
+ for ( let i = 0 ; i < parts . length ; ++ i ) {
360
+ const [ key , value ] = splitOnFirstEquals ( parts [ i ] )
361
+ if ( key . trim ( ) . toLowerCase ( ) === "host" && value ) {
362
+ return origin === value . trim ( ) . toLowerCase ( )
363
+ }
364
+ }
365
+ }
366
+
367
+ // Honor X-Forwarded-Host if present.
368
+ const xHost = getFirstHeader ( req , "x-forwarded-host" )
369
+ if ( xHost ) {
370
+ return origin === xHost . trim ( ) . toLowerCase ( )
371
+ }
372
+
373
+ // A missing host likely means the reverse proxy has not been configured to
374
+ // forward the host which means we cannot perform the check. Emit a warning
375
+ // so an admin can fix the issue.
376
+ const host = getFirstHeader ( req , "host" )
377
+ if ( ! host ) {
378
+ logger . warn ( `no host headers found; blocking request to ${ req . originalUrl } ` )
379
+ return false
380
+ }
381
+
382
+ return origin === host . trim ( ) . toLowerCase ( )
383
+ }
0 commit comments