All files / src/worker/fingerprint tampering.ts

92.85% Statements 26/28
92.85% Branches 13/14
100% Functions 6/6
92.85% Lines 26/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85        5x   5x       6x                       5x     19x 1x           18x 1x           17x 17x 3x 2x     1x   14x 14x   14x 1x               15x   15x 2x             19x 69x 69x   6x 6x 6x                  
import { FlowError } from '../errors'
import { IdentificationEvent } from './identificationClient'
import { getIp } from '../utils/headers'
 
const ALLOWED_REQUEST_TIMESTAMP_DIFF_MS = 3000
 
const methodsWithoutOrigin = ['GET', 'HEAD']
 
export class TamperingError extends FlowError {
  constructor(message: string) {
    super({
      message,
      isPrivate: true,
      httpStatus: 403,
    })
  }
}
 
type TamperingHandler = {
  verify: (event: IdentificationEvent, request: Request) => void | Promise<void>
}
 
const tamperingHandlers: TamperingHandler[] = [
  {
    verify: (event) => {
      if (event.replayed) {
        throw new TamperingError('Identification request was replayed.')
      }
    },
  },
  {
    verify: (event) => {
      if (new Date().valueOf() - event.timestamp.valueOf() > ALLOWED_REQUEST_TIMESTAMP_DIFF_MS) {
        throw new TamperingError('Old identification request, potential replay attack.')
      }
    },
  },
  {
    verify: (event, request) => {
      const originHeader = request.headers.get('origin')
      if (!originHeader) {
        if (methodsWithoutOrigin.includes(request.method)) {
          return
        }
 
        throw new TamperingError('Missing origin header, potential tampering or malformed request.')
      }
      const origin = new URL(originHeader).origin
      const identificationOrigin = new URL(event.url).origin
 
      if (origin !== identificationOrigin) {
        throw new TamperingError(
          `Unexpected origin (${origin} is not ${identificationOrigin}), potential replay attack.`
        )
      }
    },
  },
  {
    verify: async (event, request) => {
      const requestIp = await getIp(request.headers)
 
      if (requestIp !== event.ip_address) {
        throw new TamperingError('Unexpected IP address, potential replay attack.')
      }
    },
  },
]
 
export async function handleTampering(event: IdentificationEvent, request: Request) {
  for (const handler of tamperingHandlers) {
    try {
      await handler.verify(event, request)
    } catch (error) {
      Eif (error instanceof TamperingError) {
        console.error('Tampering verification failed:', error.message, event)
        throw error
      }
 
      console.error('Error verifying tampering protection:', error)
 
      throw new TamperingError('Tampering verification failed.')
    }
  }
}