Sessions and security

Three keywords cover the web-security basics; each is implemented in C inside the dispatcher, so none of them add a Perl frame to the request.

Sessions

session
    secret   => secret('session_key'),   # from the secrets system
    expires  => '7d',
    samesite => 'Lax';

$c->session is then a hashref, written back to an HMAC-SHA256-signed cookie when it changes:

post '/login' => sub {
    my ($c) = @_;
    $c->session->{user_id} = $user->id;   # signed into the cookie
    $c->redirect('/');
};

get '/me' => sub {
    my ($c) = @_;
    my $id = $c->session->{user_id} or return $c->redirect('/login');
    $c->json({ id => $id });
};

post '/logout' => sub { my ($c) = @_; $c->session_expire; $c->redirect('/') };

Source the key from the secrets system, never a literal. Options: secret, cookie (default punk.sid), expires, path, domain, secure, httponly (default on), samesite (default Lax). Also configurable from punk.yml.

CSRF

session secret => secret('session_key');
csrf;

Single-use tokens over the session: every unsafe request must carry a live token, and using one spends it.

get '/edit' => sub {
    my ($c) = @_;
    return $c->render('edit', { csrf => $c->csrf_field });
};

post '/edit' => sub {
    my ($c) = @_;          # only runs if the token was good
    ...;
};
<form method="post" action="/edit">
  {% raw csrf %}
  ...
</form>

$c->csrf_field is the hidden input, $c->csrf_token the bare value; the token is also mirrored into a script-readable cookie for fetch calls. Options: keep (how many live tokens to allow) and exempt (path prefixes to skip - webhooks, callbacks).

CORS

cors;                                   # a public API: *, no credentials
cors origins => [ 'https://app.example.com' ], credentials => 1,
     paths   => [ '/api' ];

Handled inside the dispatcher: preflights are answered before routing - no OPTIONS route needed - and the headers reach every response, including the 404s and 405s that never build a context. Access-Control-Allow-Methods comes from the router itself, so it cannot promise a method the application does not serve.

The rest of the posture

  • Controller targets, template engines and spec mounts all resolve at boot - a misconfigured app refuses to start rather than serve.
  • Secrets are references resolved from outside the config file, shown as [redacted] everywhere - see Config and secrets.
  • Static and markdown mounts refuse .. path segments.