Back

SAS-2: Broken Authentication - Code Examples

Each pair below shows a vulnerable configuration and the secure version for the same scenario. The examples focus on the failures that dominate real serverless findings: entry points that bypass the gateway, event triggers trusted as "internal," decode-only token checks, and unauthenticated service-to-service calls.

1. Lambda Function URL (serverless.yml)

Vulnerable

functions:
  adminReport:
    handler: handler.adminReport
    url:
      authorizer: none          # AuthType NONE - public endpoint, bypasses API Gateway
    # A random-looking lambda-url host is the ONLY thing standing between
    # the internet and privileged reporting logic.

Secure

functions:
  adminReport:
    handler: handler.adminReport
    url:
      authorizer: aws_iam       # callers must sign requests (SigV4); IAM verifies principal
    # For end-user access, prefer routing through API Gateway with a JWT
    # authorizer instead of exposing a Function URL at all.

2. API Gateway Authorizer Coverage (serverless.yml)

Vulnerable

provider:
  httpApi:
    authorizers:
      cognitoAuth:
        type: jwt
        identitySource: $request.header.Authorization
        issuerUrl: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXX
        audience: [ 6f0abc123clientid ]

functions:
  getProfile:
    handler: handler.getProfile
    events:
      - httpApi: { path: /profile, method: get, authorizer: { name: cognitoAuth } }
  exportData:
    handler: handler.exportData
    events:
      - httpApi: { path: /export, method: get }   # NO authorizer - silently public

Secure

functions:
  getProfile:
    handler: handler.getProfile
    events:
      - httpApi: { path: /profile, method: get, authorizer: { name: cognitoAuth } }
  exportData:
    handler: handler.exportData
    events:
      - httpApi:
          path: /export
          method: get
          authorizer: { name: cognitoAuth }        # every route carries an authorizer

3. JWT Validation in a Lambda Authorizer (Node.js)

Vulnerable

// Decodes the token but never verifies it - claims are attacker-controlled
exports.verify = async (event) => {
  const token = (event.headers.authorization || '').replace(/^Bearer /, '');
  const body = token.split('.')[1];
  const claims = JSON.parse(Buffer.from(body, 'base64').toString());  // decode only
  if (claims.role === 'admin') {
    return { isAuthorized: true, context: { sub: claims.sub } };      // forged easily
  }
  return { isAuthorized: false };
};

Secure

// Verifies signature + exp + aud + iss against the provider's JWKS
const { createRemoteJWKSet, jwtVerify } = require('jose');
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URI));

exports.verify = async (event) => {
  const token = (event.headers.authorization || '').replace(/^Bearer /, '');
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer:   process.env.EXPECTED_ISSUER,     // iss
      audience: process.env.EXPECTED_AUDIENCE,   // aud
      // signature and exp are enforced by jwtVerify; alg 'none' is rejected
    });
    return { isAuthorized: true, context: { sub: payload.sub, scope: payload.scope } };
  } catch {
    return { isAuthorized: false };              // any failure denies access
  }
};

4. Event-Triggered Function (Python)

Vulnerable

# Trusts the S3 event because it "came from AWS" - the uploader may be untrusted
def handler(event, context):
    rec = event['Records'][0]
    key = rec['s3']['object']['key']
    process_import(key)          # privileged: parses file, writes to prod DB
    grant_entitlements(key)      # acts with no authenticated actor behind it

Secure

import os
TRUSTED_BUCKET = os.environ['TRUSTED_BUCKET']

def handler(event, context):
    rec = event['Records'][0]
    bucket = rec['s3']['bucket']['name']
    if bucket != TRUSTED_BUCKET:              # confirm the expected source
        raise Exception('event from unexpected bucket')
    key = rec['s3']['object']['key']
    validate_object(bucket, key)              # scan/validate BEFORE processing
    # Where the actor matters, require a verified identity in the object metadata
    # (e.g. a signed token) rather than inferring trust from the trigger type.
    process_import(bucket, key)

5. Service-to-Service Invocation (serverless.yml + Node.js)

Vulnerable

# Callee trusts a single shared static key passed in a header
functions:
  internalSync:
    handler: sync.handler
    environment:
      SHARED_KEY: super-secret-static-value    # same across envs, never rotated

# sync.handler:
if (event.headers['x-api-key'] === process.env.SHARED_KEY) { /* allow */ }
// leaks once -> every caller is authenticated; no per-principal identity

Secure

# Caller is granted invoke on ONLY the one function; IAM/SigV4 authenticates it
functions:
  caller:
    handler: caller.handler
    iamRoleStatements:
      - Effect: Allow
        Action: lambda:InvokeFunction
        Resource: arn:aws:lambda:us-east-1:1234:function:internalSync
  internalSync:
    handler: sync.handler
    # No shared secret. The platform verifies the calling principal via IAM;
    # internalSync reads the caller identity from the request context, not a header.

What Changed, and Why

WeaknessVulnerableSecure
Entry pointFunction URL AuthType: NONE, bypasses gatewayaws_iam auth, or routed through an authorizer
Fleet coverageSome routes have an authorizer, others noneEvery route carries the same authorizer
Token validationDecoded only; claims trustedSignature + exp + aud + iss verified against JWKS
Event triggersPayload trusted as "internal"Source confirmed, input validated, identity verified
Service-to-serviceShared static key in a headerIAM/SigV4 principal, least-privilege invoke

Next Steps