Skip to content

Bug: Base64Encoded lets malformed base64 escape safeParse and leaks an unhandled rejection #5651

Description

@svozza

Expected Behavior

safeParse should never throw. A Kinesis record whose kinesis.data is not valid base64 should produce { success: false, error: ParseError, originalEvent } from KinesisDataStreamSchema.safeParse, KinesisEnvelope.safeParse, parse(event, undefined, KinesisDataStreamSchema, true), and from the decorator or Middy middleware in safeParse mode. The same applies to any customer schema built with the Base64Encoded helper.

Current Behavior

Base64Encoded is z.string().transform(...) with no base64 validation, and the transform calls fromBase64 outside a try. fromBase64 throws a TypeError for bad padding or invalid characters, and Zod 4 does not catch exceptions thrown inside transforms, so:

  • Base64Encoded(z.any()).safeParse('not base64!!') throws TypeError: Invalid base64 string.
  • KinesisDataStreamSchema.safeParse(event) and KinesisEnvelope.safeParse(event, schema) throw the same TypeError.
  • parse(event, undefined, KinesisDataStreamSchema, true) throws ParseError: Schema parsing supports only synchronous validation and leaks an unhandled promise rejection. Zod's ~standard.validate catches the synchronous throw and falls back to safeParseAsync, which returns a rejecting Promise. parse() sees a Promise, throws the misleading sync-only error, and never attaches a handler to the rejected promise. On the Lambda Node.js runtime this surfaces as Runtime.UnhandledPromiseRejection. The /* v8 ignore next */ on that branch in parser.ts marks it as unreachable, but it is reachable with the library's own built-in schema.
  • The Middy middleware with safeParse: true throws from before instead of handing the handler a failed result.

CloudWatchLogsSchema and KinesisFirehoseRecordSchema already guard with z.base64() before decoding and are not affected.

Code snippet

import { parse } from '@aws-lambda-powertools/parser';
import { Base64Encoded } from '@aws-lambda-powertools/parser/helpers';
import { KinesisDataStreamSchema } from '@aws-lambda-powertools/parser/schemas/kinesis';
import { z } from 'zod';

// 1. helper alone
Base64Encoded(z.any()).safeParse('not base64!!');
// throws TypeError: Invalid base64 string. (expected { success: false })

// 2. built-in schema through parse() with safeParse
const event = {
  Records: [
    {
      eventSource: 'aws:kinesis',
      eventVersion: '1.0',
      eventID: 'shardId-000000000006:49590338271490256608559692538361571095921575989136588898',
      eventName: 'aws:kinesis:record',
      awsRegion: 'us-east-2',
      invokeIdentityArn: 'arn:aws:iam::123456789012:role/lambda-role',
      eventSourceARN: 'arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream',
      kinesis: {
        kinesisSchemaVersion: '1.0',
        partitionKey: '1',
        sequenceNumber: '49590338271490256608559692538361571095921575989136588898',
        approximateArrivalTimestamp: 1545084650.987,
        data: 'not base64!!',
      },
    },
  ],
};

parse(event, undefined, KinesisDataStreamSchema, true);
// throws ParseError: Schema parsing supports only synchronous validation
// and the process emits an unhandledRejection: TypeError: Invalid base64 string.

Steps to Reproduce

  1. Create a Node.js 22.x or 24.x project with @aws-lambda-powertools/parser and zod@4.
  2. Run the snippet above with node --unhandled-rejections=strict.
  3. Observe the TypeError escaping safeParse in step 1, and the ParseError plus a process-level unhandled rejection in step 2.
  4. Wire the same event through parser({ schema: KinesisDataStreamSchema, safeParse: true }) as a Middy middleware and observe before throwing instead of setting request.event to a failed result.

Possible Solution

  • In Base64Encoded, start the chain with z.base64() instead of z.string() so invalid input becomes a normal Zod issue before the transform runs, matching CloudWatchLogsSchema and KinesisFirehoseRecordSchema. Alternatively wrap the fromBase64 call and report via ctx.addIssue with fatal: true.
  • In parse(), when ~standard.validate returns a Promise, attach a handler before throwing so a rejected promise from Zod's async fallback cannot leak. Dropping the v8 ignore and covering the branch with a test would keep it honest.

Powertools for AWS Lambda (TypeScript) version

2.35.0 (main at 9bab0299)

AWS Lambda function runtime

22.x, 24.x

Packaging format used

npm

Execution logs

 FAIL  parse() with safeParse never throws when a Kinesis record carries malformed base64
ParseError: Schema parsing supports only synchronous validation
 ❯ parse src/parser.ts:124:11

⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
TypeError: Invalid base64 string.
 ❯ fromBase64 ../commons/lib/esm/fromBase64.js:27:15
 ❯ Object.transform src/helpers/index.ts:108:38
 ❯ inst._zod.parse ../../node_modules/zod/v4/classic/schemas.js:1182:28
 ❯ handlePipeResult ../../node_modules/zod/v4/core/schemas.js:2174:22

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions