NAME

Punk::CORS - cross-origin resource sharing

SYNOPSIS

package MyApp;
use Punk;

cors;                                    # a public API: * , no credentials

cors origins     => [ 'https://app.example.com' ],
     credentials => 1,
     headers     => [qw(Content-Type X-CSRF-Token)],
     expose      => [qw(X-Request-Id)],
     max_age     => 600,
     paths       => [ '/api' ];

DESCRIPTION

Answers preflights and adds the cross-origin headers, from inside the C dispatcher.

Both halves happen where they have to. A preflight is answered before routing, because OPTIONS on a path with no OPTIONS route would otherwise be the router's 405. Responses are decorated on the way out of the dispatcher rather than in an after-dispatch hook, because 404 and 405 never build a context and would otherwise go out bare - which is exactly when a browser most needs to be told it may read the status.

What it is not

CORS is not access control. It tells a browser what script on another origin may read; nothing else honours it, and curl has never cared. An endpoint that must not be reached by a given caller needs a guard, not a header.

In particular it is not a replacement for Punk::CSRF. A credentialed cross-origin POST that CORS permits still has to carry a live token. The two answer different questions - "may this script read the reply" and "did this request come from our own page" - and an application that takes cookies wants both.

THE KEYWORD

  • origins - '*', a single origin, an arrayref of exact origins, or a coderef called with the request's origin. The coderef may return true (echo the origin) or a replacement origin. Default '*'.

  • credentials - send Access-Control-Allow-Credentials: true. Requires explicit origins: browsers refuse credentials with '*', and reflecting whatever origin arrives while allowing credentials is the classic CORS hole - every site gets to read your users' authenticated responses. The keyword croaks at to_app rather than let it be written.

  • headers - the Access-Control-Allow-Headers list for preflights. Omitted, the requested headers are echoed.

  • expose - response headers script may read beyond the safelisted ones.

  • max_age - how long a preflight may be cached, in seconds (default 600).

  • paths - path prefixes CORS applies to. paths => ['/api'] is how an application serves HTML same-origin and its API cross-origin.

  • methods - override the preflight's method list. See below for why you probably should not.

cors 0 turns it off. It also reads a cors: block from config/punk.yml.

Allow-Methods comes from the router

A preflight is answered with the methods that path actually serves, taken from the same routing table that produces Allow on a 405 - so it includes API operations and can never promise a method the application does not have. A preflight for a method the path does not answer is refused with 403 rather than waved through to fail later.

methods overrides this for the case the router cannot see: a mounted PSGI application whose own routes are opaque to Punk.

Vary

Vary: Origin goes on every response whose Allow-Origin depends on the request's origin - anything but a flat '*'. Without it a shared cache will happily serve one origin's response to another, which is a cache-poisoning bug wearing a CORS costume.

SEE ALSO

Punk, Punk::CSRF.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)