}
$self->hook(after_dispatch => sub {
my ($c) = @_;
- if ($c->req->is_secure
- && ! $c->res->headers->strict_transport_security
+ my ($req, $res) = ($c->req, $c->res);
+
+ if ( $req->is_secure
+ && !$res->headers->strict_transport_security
&& Bugzilla->params->{'strict_transport_security'} ne 'off')
{
my $sts_opts = 'max-age=' . MAX_STS_AGE;
if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') {
$sts_opts .= '; includeSubDomains';
}
- $c->res->headers->strict_transport_security($sts_opts);
+ $res->headers->strict_transport_security($sts_opts);
+ }
+
+ # Add X-Frame-Options header to prevent framing and subsequent
+ # possible clickjacking problems.
+ unless ($c->url_is_attachment_base) {
+ $res->headers->header('X-frame-options' => 'SAMEORIGIN');
+ }
+
+ # Add X-XSS-Protection header to prevent simple XSS attacks
+ # and enforce the blocking (rather than the rewriting) mode.
+ $res->headers->header('X-xss-protection' => '1; mode=block');
+
+ # Add X-Content-Type-Options header to prevent browsers sniffing
+ # the MIME type away from the declared Content-Type.
+ $res->headers->header('X-content-type-options' => 'nosniff');
+
+ if (length($req->url->to_abs->to_string) > 8000) {
+ $res->headers->header('Referrer-policy' => 'origin');
+ }
+ else {
+ $res->headers->header('Referrer-policy' => 'same-origin');
+ }
+
+ unless ($res->headers->content_security_policy) {
+ if (my $csp = $c->content_security_policy) {
+ $res->headers->header($csp->header_name, $csp->value);
+ }
}
});
Bugzilla::WebService::Server::REST->preload;
my ($class, $name, $file) = @_;
my $package = __PACKAGE__ . "::$name", my $inner_name = "_$name";
my $content = path(bz_locations->{cgi_path}, $file)->slurp;
- $content = "package $package; $content";
+ $content = "package $package; local our \$C = \$Bugzilla::App::CGI::C; $content";
my %options = (package => $package, file => $file, line => 1, no_defer => 1,);
die "Tried to load $file more than once" if $SEEN{$file}++;
my $inner = quote_sub $inner_name, $content, {}, \%options;
}
}
);
+ $app->helper(
+ 'url_is_attachment_base' => sub {
+ my ($c, $id) = @_;
+ return 0 unless Bugzilla::Util::use_attachbase();
+ my $attach_base = Bugzilla->localconfig->{'attachment_base'};
+
+ # If we're passed an id, we only want one specific attachment base
+ # for a particular bug. If we're not passed an ID, we just want to
+ # know if our current URL matches the attachment_base *pattern*.
+ my $regex;
+ if ($id) {
+ $attach_base =~ s/\%bugid\%/$id/;
+ $regex = quotemeta($attach_base);
+ }
+ else {
+ # In this circumstance we run quotemeta first because we need to
+ # insert an active regex meta-character afterward.
+ $regex = quotemeta($attach_base);
+ $regex =~ s/\\\%bugid\\\%/\\d+/;
+ }
+ $regex = "^$regex";
+ return ($c->req->url->to_abs =~ $regex) ? 1 : 0;
+ }
+ );
+
+ $app->helper(
+ 'content_security_policy' => sub {
+ my ($c, %add_params) = @_;
+ my $stash = $c->stash;
+ if (%add_params || !$stash->{Bugzilla_csp}) {
+ my %params = DEFAULT_CSP();
+ delete $params{report_only} if %add_params && !$add_params{report_only};
+ delete $params{report_only} if !$c->isa('Bugzilla::App::CGI');
+ foreach my $key (keys %add_params) {
+ if (defined $add_params{$key}) {
+ $params{$key} = $add_params{$key};
+ }
+ else {
+ delete $params{$key};
+ }
+ }
+ $stash->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
+ }
+
+ return $stash->{Bugzilla_csp};
+ }
+ );
+ $app->helper(
+ 'csp_nonce' => sub {
+ my ($c) = @_;
+
+ my $csp = $c->content_security_policy;
+ return $csp->has_nonce ? $csp->nonce : '';
+ }
+ );
$app->helper(
'bz_include' => sub {
*AUTOLOAD = \&CGI::AUTOLOAD;
}
-sub DEFAULT_CSP {
- my %policy = (
- default_src => ['self'],
- script_src =>
- ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'],
- frame_src => [
- # This is for extensions/BMO/web/js/firefox-crash-table.js
- 'https://crash-stop-addon.herokuapp.com',
- ],
- worker_src => ['none',],
- img_src => ['self', 'blob:', 'https://secure.gravatar.com'],
- style_src => ['self', 'unsafe-inline'],
- object_src => ['none'],
- connect_src => [
- 'self',
-
- # This is for extensions/BMO/web/js/firefox-crash-table.js
- 'https://product-details.mozilla.org',
-
- # This is for extensions/GoogleAnalytics using beacon or XHR
- 'https://www.google-analytics.com',
-
- # This is from extensions/OrangeFactor/web/js/orange_factor.js
- 'https://treeherder.mozilla.org/api/failurecount/',
- ],
- form_action => [
- 'self',
-
- # used in template/en/default/search/search-google.html.tmpl
- 'https://www.google.com/search'
- ],
- frame_ancestors => ['none'],
- report_only => 1,
- );
- if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) {
- push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize',
- 'https://github.com/login';
- }
-
- return %policy;
-}
-
-# Because show_bug code lives in many different .cgi files,
-# we needed a centralized place to define the policy.
-# normally the policy would just live in one .cgi file.
-# Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant.
-sub SHOW_BUG_MODAL_CSP {
- my ($bug_id) = @_;
- my %policy = (
- script_src => [
- 'self', 'nonce',
- 'unsafe-inline', 'unsafe-eval',
- 'https://www.google-analytics.com'
- ],
- img_src => ['self', 'https://secure.gravatar.com'],
- media_src => ['self'],
- connect_src => [
- 'self',
-
- # This is for extensions/BMO/web/js/firefox-crash-table.js
- 'https://product-details.mozilla.org',
-
- # This is for extensions/GoogleAnalytics using beacon or XHR
- 'https://www.google-analytics.com',
-
- # This is from extensions/OrangeFactor/web/js/orange_factor.js
- 'https://treeherder.mozilla.org/api/failurecount/',
- ],
- frame_src => [
- 'self',
-
- # This is for extensions/BMO/web/js/firefox-crash-table.js
- 'https://crash-stop-addon.herokuapp.com',
- ],
- worker_src => ['none',],
- );
- if (use_attachbase() && $bug_id) {
- my $attach_base = Bugzilla->localconfig->{'attachment_base'};
- $attach_base =~ s/\%bugid\%/$bug_id/g;
- push @{$policy{img_src}}, $attach_base;
- push @{$policy{media_src}}, $attach_base;
- }
-
- return %policy;
-}
-
sub _init_bz_cgi_globals {
my $invocant = shift;
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
# Redirect to urlbase if we are not viewing an attachment.
- if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
- DEBUG(
- "Redirecting to urlbase because the url is in the attachment base and not attachment.cgi"
- );
- $self->redirect_to_urlbase();
+ if (my $C = $Bugzilla::App::CGI::C) {
+ if ($C->url_is_attachment_base and $script ne 'attachment.cgi') {
+ DEBUG(
+ "Redirecting to urlbase because the url is in the attachment base and not attachment.cgi"
+ );
+ $self->redirect_to_urlbase();
+ }
}
# Check for errors
}
}
-sub content_security_policy {
- my ($self, %add_params) = @_;
- if (%add_params || !$self->{Bugzilla_csp}) {
- my %params = DEFAULT_CSP;
- delete $params{report_only} if %add_params && !$add_params{report_only};
- foreach my $key (keys %add_params) {
- if (defined $add_params{$key}) {
- $params{$key} = $add_params{$key};
- }
- else {
- delete $params{$key};
- }
- }
- $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params);
- }
-
- return $self->{Bugzilla_csp};
-}
-
-sub csp_nonce {
- my ($self) = @_;
-
- my $csp = $self->content_security_policy;
- return $csp->has_nonce ? $csp->nonce : '';
-}
-
# We want this sorted plus the ability to exclude certain params
sub canonicalise_query {
my ($self, @exclude) = @_;
$headers{'-cookie'} = $self->{Bugzilla_cookie_list};
}
- # Add Strict-Transport-Security (STS) header if this response
- # is over SSL and the strict_transport_security param is turned on.
- if ( $self->https
- && !$self->url_is_attachment_base
- && Bugzilla->params->{'strict_transport_security'} ne 'off')
- {
- my $sts_opts = 'max-age=' . MAX_STS_AGE;
- if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') {
- $sts_opts .= '; includeSubDomains';
- }
- $headers{'-strict_transport_security'} = $sts_opts;
- }
-
- # Add X-Frame-Options header to prevent framing and subsequent
- # possible clickjacking problems.
- unless ($self->url_is_attachment_base) {
- $headers{'-x_frame_options'} = 'SAMEORIGIN';
- }
-
if ($self->{'_content_disp'}) {
$headers{'-content_disposition'} = $self->{'_content_disp'};
}
- # Add X-XSS-Protection header to prevent simple XSS attacks
- # and enforce the blocking (rather than the rewriting) mode.
- $headers{'-x_xss_protection'} = '1; mode=block';
-
- # Add X-Content-Type-Options header to prevent browsers sniffing
- # the MIME type away from the declared Content-Type.
- $headers{'-x_content_type_options'} = 'nosniff';
-
- # Add Referrer-Policy (sic) header to prevent browsers sending
- # Referer (sic) headers to external websites.
- $headers{'-referrer_policy'} = 'same-origin';
-
- Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers});
$self->{_header_done} = 1;
if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) {
- if ($self->should_block_referrer) {
- $headers{'-referrer_policy'} = 'origin';
- }
- my $csp = $self->content_security_policy;
- if (defined $csp && !$csp->disable) {
- $csp->add_cgi_headers(\%headers);
- }
-
my @fonts = (
"skins/standard/fonts/FiraMono-Regular.woff2?v=3.202",
"skins/standard/fonts/FiraSans-Bold.woff2?v=4.203",
exit;
}
-sub url_is_attachment_base {
- my ($self, $id) = @_;
- return 0 if !use_attachbase() or !i_am_cgi();
- my $attach_base = Bugzilla->localconfig->{'attachment_base'};
-
- # If we're passed an id, we only want one specific attachment base
- # for a particular bug. If we're not passed an ID, we just want to
- # know if our current URL matches the attachment_base *pattern*.
- my $regex;
- if ($id) {
- $attach_base =~ s/\%bugid\%/$id/;
- $regex = quotemeta($attach_base);
- }
- else {
- # In this circumstance we run quotemeta first because we need to
- # insert an active regex meta-character afterward.
- $regex = quotemeta($attach_base);
- $regex =~ s/\\\%bugid\\\%/\\d+/;
- }
- $regex = "^$regex";
- return ($self->url =~ $regex) ? 1 : 0;
-}
-
sub set_dated_content_disp {
my ($self, $type, $prefix, $ext) = @_;
To remove (expire) a cookie, use C<remove_cookie>.
-=item C<content_security_policy>
-
-Set a Content Security Policy for the current request. This is a no-op if the 'csp' feature
-is not available. The arguments to this method are passed to the constructor of L<Bugzilla::CGI::ContentSecurityPolicy>,
-consult that module for a list of what directives are supported.
-
-=item C<csp_nonce>
-
-Returns a CSP nonce value if CSP is available and 'nonce' is listed as a source in a CSP *_src directive.
-
-If there is no nonce used, or CSP is not available, this returns the empty string.
-
=item C<remove_cookie>
This is a wrapper around send_cookie, setting an expiry date in the past,
return $self->$method;
}
-sub header_names {
+sub header_name {
my ($self) = @_;
- my @names = ('Content-Security-Policy');
+ my $name = 'Content-Security-Policy';
if ($self->report_only) {
- return map { $_ . '-Report-Only' } @names;
+ return $name . '-Report-Only';
}
else {
- return @names;
- }
-}
-
-sub add_cgi_headers {
- my ($self, $headers) = @_;
- return if $self->disable;
- foreach my $name ($self->header_names) {
- $headers->{"-$name"} = $self->value;
+ return $name;
}
}
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
+Rather than use this module, scripts should call $C->content_security_policy() which constructs the CSP headers
and registers them for the current request.
See L<Bugzilla::CGI> for details.
REMOTE_FILE
LOCAL_FILE
+ DEFAULT_CSP
+ SHOW_BUG_MODAL_CSP
+
bz_locations
IS_NULL
};
}
+sub DEFAULT_CSP {
+ my %policy = (
+ default_src => ['self'],
+ script_src =>
+ ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'],
+ frame_src => [
+ # This is for extensions/BMO/web/js/firefox-crash-table.js
+ 'https://crash-stop-addon.herokuapp.com',
+ ],
+ worker_src => ['none',],
+ img_src => ['self', 'blob:', 'https://secure.gravatar.com'],
+ style_src => ['self', 'unsafe-inline'],
+ object_src => ['none'],
+ connect_src => [
+ 'self',
+
+ # This is for extensions/BMO/web/js/firefox-crash-table.js
+ 'https://product-details.mozilla.org',
+
+ # This is for extensions/GoogleAnalytics using beacon or XHR
+ 'https://www.google-analytics.com',
+
+ # This is from extensions/OrangeFactor/web/js/orange_factor.js
+ 'https://treeherder.mozilla.org/api/failurecount/',
+ ],
+ form_action => [
+ 'self',
+
+ # used in template/en/default/search/search-google.html.tmpl
+ 'https://www.google.com/search'
+ ],
+ frame_ancestors => ['none'],
+ report_only => 1,
+ );
+ if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) {
+ push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize',
+ 'https://github.com/login';
+ }
+
+ return %policy;
+}
+
+# Because show_bug code lives in many different .cgi files,
+# we needed a centralized place to define the policy.
+# normally the policy would just live in one .cgi file.
+# Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant.
+sub SHOW_BUG_MODAL_CSP {
+ my ($bug_id) = @_;
+ my %policy = (
+ script_src => [
+ 'self', 'nonce',
+ 'unsafe-inline', 'unsafe-eval',
+ 'https://www.google-analytics.com'
+ ],
+ img_src => ['self', 'https://secure.gravatar.com'],
+ media_src => ['self'],
+ connect_src => [
+ 'self',
+
+ # This is for extensions/BMO/web/js/firefox-crash-table.js
+ 'https://product-details.mozilla.org',
+
+ # This is for extensions/GoogleAnalytics using beacon or XHR
+ 'https://www.google-analytics.com',
+
+ # This is from extensions/OrangeFactor/web/js/orange_factor.js
+ 'https://treeherder.mozilla.org/api/failurecount/',
+ ],
+ frame_src => [
+ 'self',
+
+ # This is for extensions/BMO/web/js/firefox-crash-table.js
+ 'https://crash-stop-addon.herokuapp.com',
+ ],
+ worker_src => ['none',],
+ );
+ if (Bugzilla::Util::use_attachbase() && $bug_id) {
+ my $attach_base = Bugzilla->localconfig->{'attachment_base'};
+ $attach_base =~ s/\%bugid\%/$bug_id/g;
+ push @{$policy{img_src}}, $attach_base;
+ push @{$policy{media_src}}, $attach_base;
+ }
+
+ return %policy;
+}
+
+
# This makes us not re-compute all the bz_locations data every time it's
# called.
BEGIN { memoize('_bz_locations') }
'current_language' => sub { return Bugzilla->current_language; },
'script_nonce' => sub {
- my $cgi = Bugzilla->cgi;
- return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : '';
+ my $C = $Bugzilla::App::CGI::C or return '';
+ return $C->csp_nonce ? sprintf('nonce="%s"', $C->csp_nonce) : '';
},
# If an sudo session is in progress, this is the user who
&& (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
{
do_ssl_redirect_if_required();
- if ($cgi->url_is_attachment_base) {
+ if ($C->url_is_attachment_base) {
$cgi->redirect_to_urlbase;
}
Bugzilla->login();
my $path = 'attachment.cgi?' . join('&', @args);
# Make sure the attachment is served from the correct server.
- if ($cgi->url_is_attachment_base($bug_id)) {
+ if ($C->url_is_attachment_base($bug_id)) {
# No need to validate the token for public attachments. We cannot request
# credentials as we are on the alternate host.
delete_token($token);
}
}
- elsif ($cgi->url_is_attachment_base) {
+ elsif ($C->url_is_attachment_base) {
# If we come here, this means that each bug has its own host
# for attachments, and that we are trying to view one attachment
Bugzilla::Hook::process('show_bug_format', $show_bug_format);
if ($show_bug_format->{format} eq 'modal') {
- $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bugid));
+ $C->content_security_policy(SHOW_BUG_MODAL_CSP($bugid));
}
print $cgi->header();
Bugzilla::Hook::process('show_bug_format', $show_bug_format);
if ($show_bug_format->{format} eq 'modal') {
- $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
+ $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug->id));
}
print $cgi->header();
Bugzilla::Hook::process('show_bug_format', $show_bug_format);
if ($show_bug_format->{format} eq 'modal') {
- $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
+ $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug->id));
}
print $cgi->header();
local our $template = Bugzilla->template;
local our $vars = {};
my $dbh = Bugzilla->dbh;
-$cgi->content_security_policy(report_only => 0);
+$C->content_security_policy(report_only => 0);
my $user = Bugzilla->login(LOGIN_REQUIRED);
}
else {
my $template = Bugzilla->template;
- $cgi->content_security_policy(
+ $C->content_security_policy(
script_src => ['self', 'https://www.google-analytics.com']);
# Return the appropriate HTTP response headers.
if ($user->setting('ui_experiments') eq 'on') {
Bugzilla->cgi->content_security_policy(
- Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
+ SHOW_BUG_MODAL_CSP($bug->id));
}
print $cgi->header();
$template->process($format->{'template'}, $vars)
Bugzilla::Hook::process('show_bug_format', $format_params);
if ($format_params->{format} eq 'modal') {
my $bug_id = $vars->{bug} ? $vars->{bug}->id : undef;
- $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id));
+ $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug_id));
}
my $format = $template->get_format(
"bug/show",
my $template = Bugzilla->template;
my $vars = {};
-$cgi->content_security_policy(report_only => 0);
+$C->content_security_policy(report_only => 0);
# Go straight back to query.cgi if we are adding a boolean chart.
if (grep(/^cmd-/, $cgi->param())) {
if ($format_params->{format} eq 'modal') {
my $bug_id = $cgi->param('id');
detaint_natural($bug_id);
- $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id));
+ $C->content_security_policy(SHOW_BUG_MODAL_CSP($bug_id));
}
[% END %]
[% USE Bugzilla %]
- [% IF Bugzilla.cgi.should_block_referrer %]
- <meta name="referrer" content="origin">
- [% ELSE %]
- <meta name="referrer" content="origin-when-cross-origin">
- [% END %]
-
[%- js_BUGZILLA = {
config => {
basepath => basepath,
# for setting SOAPAction, which isn't used by XML-RPC.
$server->on_action(sub { $server->handle_login(WS_DISPATCH, @_) })->handle();
};
-my $C = $Bugzilla::App::CGI::C;
my ($header_str, $body) = split(/(?:\r\n\r\n|\n\n)/, $stdout, 2);
my $headers = Mojo::Headers->new;
$headers->parse("$header_str\r\n\r\n");