@@ -12,7 +12,15 @@ import { version as codeServerVersion } from "./constants"
1212import { Heart } from "./heart"
1313import { CoderSettings , SettingsProvider } from "./settings"
1414import { 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"
1624
1725/**
1826 * Base options included on every page.
@@ -308,3 +316,68 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
308316export const self = ( req : express . Request ) : string => {
309317 return normalize ( `${ req . baseUrl } ${ req . originalUrl . endsWith ( "/" ) ? "/" : "" } ` , true )
310318}
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