# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # This Source Code Form is "Incompatible With Secondary Licenses", as # defined by the Mozilla Public License, v. 2.0. package Bugzilla::CGI::ContentSecurityPolicy; use 5.14.0; use strict; use warnings; use Moo; use MooX::StrictConstructor; use Types::Standard qw(Bool Str ArrayRef); use Type::Utils; use Bugzilla::Util qw(generate_random_password); my $SRC_KEYWORD = enum['none', 'self', 'unsafe-inline', 'unsafe-eval', 'nonce']; my $SRC_URI = declare as Str, where { $_ =~ m{ ^(?: https?:// )? # optional http:// or https:// [*A-Za-z0-9.-]+ # hostname including wildcards. Possibly too permissive. (?: :[0-9]+ )? # optional port }x; }; my $SRC = $SRC_KEYWORD | $SRC_URI; my $SOURCE_LIST = ArrayRef[$SRC]; my $REFERRER_KEYWORD = enum [qw( no-referrer no-referrer-when-downgrade origin origin-when-cross-origin unsafe-url )]; my @ALL_BOOL = qw( sandbox upgrade_insecure_requests ); my @ALL_SRC = qw( default_src child_src connect_src font_src img_src media_src object_src script_src style_src ); has \@ALL_SRC => ( is => 'ro', isa => $SOURCE_LIST, predicate => 1 ); has \@ALL_BOOL => ( is => 'ro', isa => Bool, default => 0 ); has 'report_uri' => ( is => 'ro', isa => Str, predicate => 1 ); has 'base_uri' => ( is => 'ro', isa => Str, predicate => 1 ); has 'report_only' => ( is => 'ro', isa => Bool ); has 'referrer' => ( is => 'ro', isa => $REFERRER_KEYWORD, predicate => 1 ); has 'value' => ( is => 'lazy' ); has 'nonce' => ( is => 'lazy', init_arg => undef, predicate => 1 ); has 'disable' => ( is => 'ro', isa => Bool, default => 0 ); sub _has_directive { my ($self, $directive) = @_; my $method = 'has_' . $directive; return $self->$method; } sub header_names { my ($self) = @_; my @names = ('Content-Security-Policy', 'X-Content-Security-Policy', 'X-WebKit-CSP'); if ($self->report_only) { return map { $_ . '-Report-Only' } @names; } else { return @names; } } sub add_cgi_headers { my ($self, $headers) = @_; return if $self->disable; foreach my $name ($self->header_names) { $headers->{"-$name"} = $self->value; } } sub _build_value { my $self = shift; my @result; my @list_directives = (@ALL_SRC); my @boolean_directives = (@ALL_BOOL); my @single_directives = qw(report_uri base_uri); foreach my $directive (@list_directives) { next unless $self->_has_directive($directive); my @values = map { $self->_quote($_) } @{ $self->$directive }; if (@values) { push @result, join(' ', _name($directive), @values); } } foreach my $directive (@single_directives) { next unless $self->_has_directive($directive); my $value = $self->$directive; if (defined $value) { push @result, _name($directive) . ' ' . $value; } } foreach my $directive (@boolean_directives) { if ($self->$directive) { push @result, _name($directive); } } return join('; ', @result); } sub _build_nonce { return generate_random_password(48); } sub _name { my $name = shift; $name =~ tr/_/-/; return $name; } sub _quote { my ($self, $val) = @_; if ($val eq 'nonce') { return q{'nonce-} . $self->nonce . q{'}; } elsif ($SRC_KEYWORD->check($val)) { return qq{'$val'}; } else { return $val; } } 1; __END__ =head1 NAME Bugzilla::CGI::ContentSecurityPolicy - Object-oriented interface to generating CSP directives and adding them to headers. =head1 SYNOPSIS use Bugzilla::CGI::ContentSecurityPolicy; my $csp = Bugzilla::CGI::ContentSecurityPolicy->new( default_src => [ 'self' ], style_src => [ 'self', 'unsafe-inline' ], script_src => [ 'self', 'nonce' ], child_src => ['none'], report_uri => '/csp-report.cgi', referrer => 'origin-when-cross-origin', ); $csp->headers_names # returns a list of header names and depends on the value of $self->report_only $csp->value # returns the string representation of the policy. $csp->add_cgi_headers(\%hashref); # will insert entries compatible with CGI.pm's $cgi->headers() method into the provided hashref. =head1 DESCRIPTION This class provides an object interface to constructing Content Security Policies. Rather than use this module, scripts should call $cgi->content_security_policy() which constructs the CSP headers and registers them for the current request. See L for details. =head1 ATTRIBUTES Generally all CSP directives are available as attributes to the constructor, with dashes replaced by underscores. All directives that can be lists must be passed as array references, and the quoting rules for urls and keywords like 'self' or 'none' is handled automatically. =head2 report_only If this is true, then the the -Report-Only version of the headers will be produced, so nothing will be blocked. =head2 disable If this is true, no CSP headers will be used at all. =head2 base_uri The base-uri directive defines the URIs that a user agent may use as the document base URL. If this value is absent, then any URI is allowed. If this directive is absent, the user agent will use the value in the base element. =head2 child_src The child-src directive defines the valid sources for web workers and nested browsing contexts loaded using elements such as and