Skip to content

[zod-openapi][security] Unexpected validation bypass when omitting content-type  #891

@tobiasdcl

Description

@tobiasdcl

Hey there,
starting from "@hono/zod-openapi": "0.15.2" the body validation is skipped if the content-type is not matched - ref PR.

Personally I was very surprised by this as this means you can effectively bypass validations and invoke the handler as long as you just omit the content-type.
I was also not able to find any mentioning of this in the docs. I think this can be a significant security risk as it causes unexpected behaviour in the handler which might expect a validated payload.

I would argue that the mention PR should be reverted and the handler should fail if the content-type is not matched, but at least this should be clearly documented and have been marked as a breaking change.

Reproduction
import { createRoute, z, OpenAPIHono } from '@hono/zod-openapi';

const route = createRoute({
  method: 'POST',
  path: '/book',
  request: {
    // This would mitigate the issue as we enforce the usage of `application/json` content-type
    //headers: z.object({
    //  'content-type': z.literal('application/json'),
    //}),
    body: {
      content: {
        'application/json': {
          schema: z.object({
            title: z.string().startsWith('[TITLE]').min(10).max(30),
          }),
        },
      },
    },
  },
});

const app = new OpenAPIHono();

app.openapi(route, (c) => {
  const validatedBody = c.req.valid('json');

  console.log('This will only be logged when the request body passed validation, right?!', validatedBody);

  return c.json({ processed: true });
});



console.log("Case I: sending a valid request with content-type `application/json`")
/**
 * Output:
 * 
 * This will only be logged when the request body passed validation, right?! { title: '[TITLE] Hello World' }
 * { status: 200, response: { processed: true } }
 */
await app.request('/book', {
  method: 'POST',
  headers: new Headers({ 'Content-Type': 'application/json' }),
  body: JSON.stringify({ title: '[TITLE] Hello World' }),
}).then(async response => console.log({ status: response.status, response: await response.json() }));


console.log("\n\nCase II: sending a malformed request with content-type `application/json`")
/**
 * Output:
 * 
 * {
 *    status: 400,
 *    response: { success: false, error: { issues: [Array], name: 'ZodError' } }
 * }
 */
await app.request('/book', {
  method: 'POST',
  headers: new Headers({ 'Content-Type': 'application/json' }),
  body: JSON.stringify({ title: 'not valid' }),
}).then(async response => console.log({ status: response.status, response: await response.json() }));

console.log("\n\nCase III: sending a malformed request without any content-type")
/**
 * Output:
 * 
 * Case III: sending a malformed request without any content-type
 * This will only be logged when the request body passed validation, right?! {}
 * { status: 200, response: { processed: true } }
 */
await app.request('/book', {
  method: 'POST',
  body: JSON.stringify({ title: 'not valid' }),
}).then(async response => console.log({ status: response.status, response: await response.json() }));

With "@hono/zod-openapi": "0.15.1" you got the following behaviour - which I would expect:

Case I: sending a valid request with content-type `application/json`
This will only be logged when the request body passed validation, right?! { title: '[TITLE] Hello World' }
{ status: 200, response: { processed: true } }


Case II: sending a malformed request with content-type `application/json`
{
  status: 400,
  response: { success: false, error: { issues: [Array], name: 'ZodError' } }
}


Case III: sending a malformed request without any content-type
{
  status: 400,
  response: { success: false, error: { issues: [Array], name: 'ZodError' } }
}

starting from "@hono/zod-openapi": "0.15.2" the handler is invoked if the content-type is omitted:

Case I: sending a valid request with content-type `application/json`
This will only be logged when the request body passed validation, right?! { title: '[TITLE] Hello World' }
{ status: 200, response: { processed: true } }


Case II: sending a malformed request with content-type `application/json`
{
  status: 400,
  response: { success: false, error: { issues: [Array], name: 'ZodError' } }
}


// ‼️ This is the problem ‼️
Case III: sending a malformed request without any content-type
This will only be logged when the request body passed validation, right?! {}
{ status: 200, response: { processed: true } }

as a mitigation the content-type can be enforced by validating the incoming content-type header.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions