The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

URL::Signature - Tamper-proof URLs with Signed authentication

SYNOPSIS

  use URL::Signature;
  my $obj = URL::Signature->new( key => 'My secret key' );

  # code above is the same as:
  my $obj = URL::Signature->new(
          key    => 'My secret key',
          digest => 'Digest::SHA'
          length => 28,
          format => 'path',
          as     => 1, # where in the uri path should we
                       # look/place the signature.
  );

  # get a URI object with the HMAC signature attached to it
  my $url = $obj->sign( '/path/to/somewhere?data=stuff' );


  # if path is valid, get a URI object without the signature in it
  my $path = 'www.example.com/1b23094726520/some/path?data=value&other=extra';
  my $validated = $obj->validate($path);

Want to put your signatures in variables instead of the path? No problem!

  my $obj = URL::Signature->new(
          key    => 'My secret key',
          format => 'query',
          as     => 'foo', # variable name for us to
                           # look/place the signature
  );

  my $path = 'www.example.com/some/path?data=value&foo=1b23094726520&other=extra';
  my $validated = $obj->validate($path);

  my $url = $obj->sign( '/path/to/somewhere?data=stuff' );

You can also do the mangling yourself and just check Check below for some examples on how to integrate the integrity check to some "EXAMPLES" in popular Perl web frameworks.

DESCRIPTION

This module is a simple wrapper around Digest::HMAC and <URI>. It is intended to make it simple to do integrity checks on URLs (and other URIs as well).

URL Tampering?

Sometimes you want to provide dynamic resources in your server based on path or query parameters. An image server, for instance, might want to provide different sizes and effects for images like so:

  http://myserver/images/150x150/flipped/perl.png

A malicious user might take advantage of that to try and traverse through options or even DoS your application by forcing it to do tons of unnecessary processing and filesystem operations.

One way to prevent this is to sign your URLs with HMAC and a secret key. In this approach, you authenticate your URL and append the resulting code to it. The above URL could look like this:

  http://myserver/images/041da974ac0390b7340/150x150/flipped/perl.png

  or

  http://myserver/images/150x150/flipped/perl.png?k=041da974ac0390b7340

This way, whenever your server receives a request, it can check the URL to see if the provided code matches the rest of the path. If a malicious user tries to tamper with the URL, the provided code will be a mismatch to the tampered path and you'll be able to catch it early on.

It is worth noticing that, when in 'query' mode, the key order is not important for validation. That means the following URIs are all considered valid (for the same given secret key):

    foo/bar?a=1&b=2&k=SOME_KEY
    foo/bar?a=1&k=SOME_KEY&b=2
    foo/bar?b=2&k=SOME_KEY&a=1
    foo/bar?b=2&a=1&k=SOME_KEY
    foo/bar?k=SOME_KEY&a=1&b=2
    foo/var?k=SOME_KEY&b=2&a=1

METHODS

new( %attributes )

Instatiates a new object. You can set the following properties:

  • key - (REQUIRED) A string containing the secret key used to generate and validate the URIs. As a security feature, this attribute contains no default value and is mandatory. Typically, your application will fetch the secret key from a configuration file of some sort.

  • length - The size of the resulting code string to be appended in your URIs. This needs to be a positive integer, and defaults to 28. Note that, the smaller the string, the easier it is for a malicious user to brute-force it.

  • format - This module provides two different formats for URL signing: 'path' and 'query'. When set to 'path', the authentication code will be injected into (and extracted from) one of the URI's segment. When set to 'query', it will be injected/extracted as a query parameter. Default is path.

  • as - When the format is 'path', this option will specify the segment's position in which to inject/extract the authentication code. If the format is set to 'path', this option defaults to 1. When the format is 'query', this option specifies the query parameter's name, and defaults to 'k'. Other format providers might specify different defaults, so please check their documentation for details.

  • digest - The name of the module handling the message digest algorithm to be used. This is typically one of the Digest:: modules, and it must comply with the 'Digest' interface on CPAN. Defaults to Digest::SHA, which uses the SHA-1 algorithm by default.

sign( $url_string )

Receives a string containing the URL to be signed. Returns a URI object with the original URL modified to contain the authentication code.

validate( $url_string )

Receives a string containing the URL to be validated. Returns false if the URL's auth code is not a match, otherwise returns an URI object containing the original URL minus the authentication code.

Convenience Methods

Aside from sign() and validate(), there are a few other methods you may find useful:

code_for_uri( $uri_object )

Receives a URI object and returns a string containing the authentication code necessary for that object.

extract( $uri_object )

    my ($code, $new_uri) = $obj->extract( $original_uri );

Receives a URI object and returns two elements:

  • the extracted signature from the given URI

  • a new URI object just like the original minus the signature

This method is implemented by the format subclasses themselves, so you're advised to referring to their documentation for specifics.

append

    my $new_uri = $obj->append( $original_uri, $code );

Receives a URI object and the authentication code to be inserted. Returns a new URI object with the auth code properly appended, according to the requested format.

This method is implemented by the format subclasses themselves, so you're advised to referring to their documentation for specifics.

EXAMPLES

The code below demonstrates how to use URL::Signature in the real world. These are, of course, just snippets to show you possibilities, and are not meant to be rules or design patterns. Please refer to the framework's documentation and communities for best practices.

Dancer Integration

If you're using Dancer, you can create a 'before' hook to check all your routes' signatures. This example uses the 'path' format.

    use Dancer;
    use URL::Signature;

    my $validator = URL::Signature->new( key => 'my-secret-key' );

    hook 'before' => sub {
        my $uri = $validator->validate( request->uri )
                    or return send_error('forbidden', 403);

        # The following line is required only in 'path' mode, as
        # we need to remove the actual auth code from the request
        # path, otherwise Dancer won't find the real route.
        request->path( $uri->path );
    };

    get '/some/route' => sub {
            return 'Hi there, Miss Integrity!';
    };

    start;

Plack Integration

Most Perl web frameworks nowadays run on PSGI. If you're working directly with Plack, you can use something like the example below to validate your URLs. This example uses the 'path' format.

    package Plack::App::MyApp;
    use parent qw(Plack::Component);
    use URL::Signature;
    my $validator = URL::Signature->new( key => 'my-secret-key' );

    sub call {
      my ($self, $env) = @_;
      return [403, ['Content-Type' => 'text/plain', 'Content-Length' => 9], ['forbidden']]
          unless $validator->validate( $env->{REQUEST_URI} );

      return [200, ['Content-Type' => 'text/plain', 'Content-Lengh' => 10], ['Validated!']];
    }
    
    package main;
    
    my $app = Plack::App::MyApp->new->to_app;

Catalyst integration

Catalyst is arguably the most popular Perl MVC framework out there, and lets you chain your actions together for increased power and flexibility. In this example, we are using path validation in one of our controllers, and detaching to a 'default' action if the path's signature is invalid.

    sub signed :Chained('/') :PathPart('') :CaptureArgs(1) {
        my ($self, $c, $code) = @_;

        # get the key from your app's config file
        my $signer = URL::Signature->new( key => $c->config->{url_key} );

        $c->detach('default')
            unless $signer->validate( $c->req->uri->path );
    }

Now we can make signed actions anywhere in the controller by simply making sure it is chained to our 'signed' sub:

    sub some_action :Chained('signed') {
        my ($self, $c) = @_;
        ...
    }

When you need to create links to your signed routes, just use the sign() method as you normally would. If you're worried about your app's root namespace, just use your framework's uri_for('/some/path') method. Below we created a 'signed_uri_for' function in Catalyst's stash as an example (though virtually all frameworks provide a stash and a uri_for helper):

    my $signer = URL::Signature->new( key => 'my-secret-key' );
    $c->stash( signed_uri_for =>
        sub { $signer->sign( $c->uri_for(@_)->path ) }
    );

    # later on, in your code:
    my $link = $c->stash->{signed_uri_for}->( '/some/path' );

    # or even better, from within your template:
    <a href="[% signed_uri_for('/some/path') %]">Click now!</a>

Getting the signature from HTTP headers

Some argue that it's more elegant to pass the resource signature via HTTP headers, rather than altering the URL itself. URL::Signature also fits the bill, but in this case you'd use it in a slightly different way. Below is an example, using Catalyst:

    sub signed :Chained('/') {
        my ($self, $c) = @_;
        my $signer     = URL::Signature->new( key => $c->config->{url_key} );

        my $given_code = $c->req->header('X-Signature');
        my $real_code  = $signer->code_for_uri( $c->req->uri );

        $c->detach('/') unless $given_code eq $real_code;
    }

DIAGNOSTICS

Messages from the constructor:

digest must be a valid Perl class

When setting the 'digest' attribute, it must be a string containing a valid module name, like 'Digest::SHA' or 'Digest::MD5'.

length should be a positive integer

When setting the 'length' attribute, make sure its greater than zero.

format should be either 'path' or 'query'

Self-explanatory :)

in 'path' format, 'as' needs to be a non-negative integer

The 'path' mode means the auth code will be inserted as one of the URI segments. This means that, if you have a URI like:

   foo/bar/baz

Then if 'as' is 0, it will change to:

   CODE/foo/bar/baz

With 'as' set to 1 (the default for path mode), it changes to:

   foo/CODE/bar/baz

And so on. As such, the value for 'as' should be 0 or greater.

in 'query' format, 'as' needs to be a valid string

The 'query' mode means the auth code will be inserted as one of the URI variables (query parameters). This means that, if you have a URI like:

   foo/bar/baz

Then if 'as' is set to 'k' (the default for query mode), it will change to:

   foo/bar/baz?k=CODE

As such, the value for 'as' should be a valid string.

Messages from sign():

variable '%s' (reserved for auth code) found in path

When in query mode, the object will throw this exception when it tries to append the authentication code into the given URI, but finds that a variable with the same name already exists (the 'as' parameter in the constructor).

EXTENDING

When you specify a 'format' attribute, URL::Signature will try and load the proper subclass for you. For example, the 'path' format is implemented by URL::Signature::Path, and the 'query' format by URL::Signature::Query. Please follow the same convention for your custom formatters so others can use it via the URL::Signature interface.

If you wish do create a new format for URL::Signature, you'll need to implement at least extract() and append(). If you wish to mangle with constructor attributes, please do so in the BUILD(), as you would with Moose classes:

BUILD

URL::Signature will call this method on the subclass after the object is instantiated, just like Moose does. The only argument is $self, with attributes properly set into place in the internal hash for you to check.

Feel free to skim through the bundled URL::Signature formatters and use them as a base to develop your own.

CONFIGURATION AND ENVIRONMENT

URL::Signature requires no configuration files or environment variables.

BUGS AND LIMITATIONS

Please report any bugs or feature requests to bug-url-sign@rt.cpan.org, or through the web interface at http://rt.cpan.org.

SEE ALSO

AUTHOR

Breno G. de Oliveira <garu@cpan.org>

LICENCE AND COPYRIGHT

Copyright (c) 2013, Breno G. de Oliveira <garu@cpan.org>. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.

DISCLAIMER OF WARRANTY

BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.

IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.