NAME

Mojolicious::Plugin::StrictCORS - Strict and secure control over CORS

SYNOPSIS

# Mojolicious app
sub startup {
  my ($app) = @_;

  # load and configure
  $app->plugin('StrictCORS');
  $app->plugin('StrictCORS', {
    max_age => -1,
    expose  => ['X-Message']
  });

  # set app-wide CORS defaults
  $app->routes->to(cors_credentials => 1);

  # set default CORS options for nested routes
  $r = $r->under(..., { cors_origin => ['*'] }, ...);

  # set CORS options for this route (at least "origin" option must be
  # defined to allow CORS, either here or in parent routes)
  $r->get(..., { cors_origin => ['*'] }, ...);
  $r->route(...)->to(cors_origin => ['*']);

  # allow non-simple (with preflight) CORS on this route
  $r->cors(...);

  # create under to protect all nested routes
  $r = $app->routes->under_cors("/v1");

DESCRIPTION

Mojolicious::Plugin::StrictCORS is a plugin that allow you to configure Cross Origin Resource Sharing for routes in Mojolicious app.

Implements this spec: http://www.w3.org/TR/2014/REC-cors-20140116/.

This module is based on Powerman's CORS implementation: https://github.com/powerman/perl-Mojolicious-Plugin-SecureCORS But this module no longer updated, so this one wos created.

SECURITY

Don't use the lazy cors_origin => ['*'] for resources which should be available only for intranet or which behave differently when accessed from intranet - otherwise malicious website opened in browser running on workstation in intranet will get access to these resources.

Don't use the lazy cors_origin => ['*'] for resources which should be available only from some known websites - otherwise other malicious website will be able to attack your site by injecting JavaScript into the victim's browser.

Consider using under_cors() - it won't "save" you but may helps.

INTERFACE

CORS options

To allow CORS on some route you should define relevant CORS options for that route. These options will be processed automatically using "around_action" in Mojolicious hook and result in adding corresponding HTTP headers to the response.

Options should be added into default parameters for the route or it parent routes. Defining CORS options on parent route allow you to set some predefined defaults for their nested routes.

cors_origin => ['*']
cors_origin => ["http://example.com"]
cors_origin => ["https://example.com", "http://example.com:8080"]
cors_origin => [qr/\.local\z/ms]
cors_origin => undef >> (default)

This option is required to enable CORS support for the route.

Only matched origins will be allowed to process returned response (['*'] will match any origin).

When set to undef no origins will match, so it effectively disable CORS support (may be useful if you've set this option value on parent route).

cors_credentials => 1
cors_credentials => undef (default)

While handling preflight request true/false value will tell browser to send or not send credentials (cookies, http auth, SSL certificate) with actual request.

While handling simple/actual request if set to false and browser has sent credentials will disallow to process returned response.

cors_expose => ['X-Some']
cors_expose => [qw/X-Some X-Other Server/]
cors_expose => undef (default)

Allow access to these headers while processing returned response.

These headers doesn't need to be included in this option:

Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
cors_headers => ['X-Requested-With']
cors_headers => [qw/X-Requested-With Content-Type X-Some/]
cors_headers => undef (default)

Define headers which browser is allowed to send. Work only for non-simple CORS because it require preflight.

cors_methods => ['POST']
cors_methods => [qw/GET POST PUT DELETE]

This option can be used only for cors() route. It's needed in complex cases when it's impossible to automatically detect CORS option while handling preflight - see below for example.

cors

$app->routes->cors(...);

Accept same params as "route" in Mojolicious::Routes::Route.

Add handler for preflight (OPTIONS) CORS request - it's required to allow non-simple CORS requests on given path.

To be able to respond on preflight request this handler should know CORS options for requested method/path. In most cases it will be able to detect them automatically by searching for route defined for same path and HTTP method given in CORS request. Example:

$r->cors("/rpc");
$r->get("/rpc", { cors_origin => ["http://example.com"] });
$r->put("/rpc", { cors_origin => [qr/\.local\z/ms] });

But in some cases target route can't be detected, for example if you've defined several routes for same path using different conditions which can't be checked while processing preflight request because browser didn't sent enough information yet (like Content-Type: value which will be used in actual request). In this case you should manually define all relevant CORS options on preflight route - in addition to CORS options defined on target routes. Because you can't know which one of defined routes will be used to handle actual request, in case they use different CORS options you should use combined in less restrictive way options for preflight route. Example:

$r->cors("/rpc")->to(
    cors_methods      => [qw/GET POST/],
    cors_origin       => ["http://localhost", "http://example.com"],
    cors_credentials  => 1,
);
$r->any([qw(GET POST)] => "/rpc")->over(
  headers => {
    'Content-Type' => 'application/json-rpc'
  }
)->to(
  controller    => 'jsonrpc',
  action        => 'handler',

  cors_origin   => ["http://localhost"]
);
$r->post("/rpc")->over(
  headers => {
    'Content-Type' => 'application/soap+xml'
  }
)->to(
  controller  => 'soaprpc',
  action      => 'handler',

  cors_origin       => "http://example.com",
  cors_credentials  => 1
);

This route use 'headers' condition, so you can add your own handler for OPTIONS method on same path after this one, to handle non-CORS OPTIONS requests on same path.

under_cors

$route = $app->routes->under_cors(...)

Accept same params as "under" in Mojolicious::Routes::Route.

Under returned route CORS requests to any route which isn't configured for CORS (i.e. won't have cors_origin in route's default parameters) will be rendered as "403 Forbidden".

This feature should make it harder to attack your site by injecting JavaScript into the victim's browser on vulnerable website. More details: https://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity#Processing_rogue_COR:.

OPTIONS

Mojolicious::Plugin::StrictCORS supports the following options.

max_age

$app->plugin('StrictCORS', { max_age => -1 });

Value for Access-Control-Max-Age: sent by preflight OPTIONS handler. If set to -1 cache will be disabled.

Default is 3600 (1 hour).

expose

$app->plugin('StrictCORS', { expose => ['X-Message']});

Default value for Access-Control-Expose-Headers for all requests, that configured to use CORS.

Defailt is ampty array.

METHODS

Mojolicious::Plugin::StrictCORS inherits all methods from Mojolicious::Plugin and implements the following new ones.

register

$plugin->register(Mojolicious->new);
$plugin->register(Mojolicious->new, { max_age => -1 });

Register hooks in Mojolicious application.

SEE ALSO

Mojolicious.

SUPPORT

Bugs / Feature Requests

Bugs should always be submitted via the GitHub bug tracker.

https://github.com/bitnoize/mojolicious-plugin-strictcors/issues

Source Code

Feel free to fork the repository and submit pull requests.

https://github.com/bitnoize/mojolicious-plugin-strictcors

AUTHOR

Dmitry Krutikov <monstar@cpan.org>

COPYRIGHT AND LICENSE

Copyright 2020 Dmitry Krutikov.

You may distribute under the terms of either the GNU General Public License or the Artistic License, as specified in the README file.