From 68162e30b461ba317136b68865e7ff73f888cdc8 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 25 Jul 2026 22:45:12 -0400 Subject: [PATCH 01/12] Bug 1832783 - Reject oversized CGI requests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 22 +++++++++++++++ t/app-cgi-request-limit.t | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 t/app-cgi-request-limit.t diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index b8f133b007..57bce7e257 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -60,6 +60,28 @@ sub load_one { my $inner = quote_sub $inner_name, $content, {}, \%options; my $wrapper = sub { my ($c) = @_; + + if ($c->req->is_limit_exceeded) { + my $message = 'The request is too large.'; + my $content_type = $c->req->headers->content_type // ''; + if ($file eq 'post_bug.cgi' + && $content_type =~ m{^multipart/form-data\b}i) + { + my $max_size = Bugzilla->params->{maxattachmentsize}; + if ($max_size) { + my $limit = "$max_size KB"; + if ($max_size >= 1024) { + $limit = sprintf('%.2f MB', $max_size / 1024); + $limit =~ s/\.?0+ MB$/ MB/; + } + $message .= " Attachments are limited to $limit."; + } + } + + $c->res->headers->content_type('text/plain; charset=UTF-8'); + return $c->render(text => "$message\n", status => 413); + } + Bugzilla->request_cache->{mojo_controller} = $c; my $stdin = $c->_STDIN; local %ENV = $c->_ENV($file); diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t new file mode 100644 index 0000000000..a43da83454 --- /dev/null +++ b/t/app-cgi-request-limit.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl +# 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/. +use strict; +use warnings; +use 5.10.1; +use lib qw( . lib local/lib/perl5 ); + +BEGIN { + $ENV{BUGZILLA_DISABLE_HOSTAGE} = 1; + $ENV{LOG4PERL_CONFIG_FILE} = 'log4perl-t.conf'; + $ENV{MOJO_MAX_MESSAGE_SIZE} = 512; +} + +use Bugzilla::Test::MockLocalconfig (urlbase => 'http://bmo.test'); +use Bugzilla::Test::MockDB; +use Bugzilla::Test::MockParams (maxattachmentsize => 10_240); + +use Test2::V0; +use Test::Mojo; + +my $boundary = 'bugzilla-request-limit'; +my $body = join( + "\r\n", + "--$boundary", + 'Content-Disposition: form-data; name="bug_type"', + '', + 'defect', + "--$boundary", + 'Content-Disposition: form-data; name="data"; filename="large.txt"', + 'Content-Type: text/plain', + '', + 'x' x 512, + "--$boundary--", + '' +); + +my $t = Test::Mojo->new('Bugzilla::App'); +$t->post_ok( + '/post_bug.cgi' => { + 'Content-Length' => length($body), + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => $body +)->status_is(413) + ->header_is('Content-Type' => 'text/plain; charset=UTF-8') + ->content_is("The request is too large. Attachments are limited to 10 MB.\n") + ->content_unlike(qr/bug_type/i); + +done_testing; From 604bc923602d8ca53ff179558c45db7aae5d2b90 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 25 Jul 2026 23:00:27 -0400 Subject: [PATCH 02/12] Bug 1832783 - Preserve exact non-MiB attachment limits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 5 +---- t/app-cgi-request-limit.t | 12 ++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 57bce7e257..83ee320497 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -70,10 +70,7 @@ sub load_one { my $max_size = Bugzilla->params->{maxattachmentsize}; if ($max_size) { my $limit = "$max_size KB"; - if ($max_size >= 1024) { - $limit = sprintf('%.2f MB', $max_size / 1024); - $limit =~ s/\.?0+ MB$/ MB/; - } + $limit = $max_size / 1024 . ' MB' if $max_size % 1024 == 0; $message .= " Attachments are limited to $limit."; } } diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index a43da83454..b952e7cedd 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -47,4 +47,16 @@ $t->post_ok( ->content_is("The request is too large. Attachments are limited to 10 MB.\n") ->content_unlike(qr/bug_type/i); +my $config = Bugzilla::Config->new; +$config->set_param(maxattachmentsize => 2047); +$config->update; +$t->post_ok( + '/post_bug.cgi' => { + 'Content-Length' => length($body), + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => $body +)->status_is(413) + ->content_is( + "The request is too large. Attachments are limited to 2047 KB.\n"); + done_testing; From 7d44287540cdb1a22194193549b89a2dafc4e6b6 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 25 Jul 2026 23:39:13 -0400 Subject: [PATCH 03/12] Bug 1832783 - Satisfy operator critic policy Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 83ee320497..3e10acd9cf 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -70,7 +70,7 @@ sub load_one { my $max_size = Bugzilla->params->{maxattachmentsize}; if ($max_size) { my $limit = "$max_size KB"; - $limit = $max_size / 1024 . ' MB' if $max_size % 1024 == 0; + $limit = ($max_size / 1024) . ' MB' if $max_size % 1024 == 0; $message .= " Attachments are limited to $limit."; } } From e51c8e09bb72893bc2c0c726021a070e04b86ac1 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 29 Jul 2026 23:16:19 -0400 Subject: [PATCH 04/12] Bug 1832783 - Preserve request limit error contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 103 +++++++++++++++--- t/app-cgi-request-limit.t | 85 +++++++++++++-- .../en/default/global/user-error.html.tmpl | 4 + 3 files changed, 165 insertions(+), 27 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 3e10acd9cf..7c376f3c97 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -17,7 +17,15 @@ use Socket qw(AF_INET inet_aton); use Mojo::File qw(path); use English qw(-no_match_vars); use Bugzilla::App::Stdout; -use Bugzilla::Constants qw(bz_locations USAGE_MODE_BROWSER); +use Bugzilla::Constants qw( + bz_locations + ERROR_MODE_REST + USAGE_MODE_BROWSER + USAGE_MODE_REST +); +use Bugzilla::Logging; +use Bugzilla::Util qw(trim xml_quote); +use Bugzilla::WebService::Constants qw(ERROR_UNKNOWN_TRANSIENT); my %SEEN; @@ -61,22 +69,10 @@ sub load_one { my $wrapper = sub { my ($c) = @_; - if ($c->req->is_limit_exceeded) { - my $message = 'The request is too large.'; - my $content_type = $c->req->headers->content_type // ''; - if ($file eq 'post_bug.cgi' - && $content_type =~ m{^multipart/form-data\b}i) - { - my $max_size = Bugzilla->params->{maxattachmentsize}; - if ($max_size) { - my $limit = "$max_size KB"; - $limit = ($max_size / 1024) . ' MB' if $max_size % 1024 == 0; - $message .= " Attachments are limited to $limit."; - } - } - - $c->res->headers->content_type('text/plain; charset=UTF-8'); - return $c->render(text => "$message\n", status => 413); + if (_is_message_size_exceeded($c->req)) { + my $reason = $c->req->error->{message}; + WARN("Rejected oversized request for $file: $reason"); + return _render_request_too_large($c, $file); } Bugzilla->request_cache->{mojo_controller} = $c; @@ -113,6 +109,79 @@ sub load_one { return 1; } +sub _is_message_size_exceeded { + my ($request) = @_; + my $error = $request->error; + return $request->is_limit_exceeded + && ref $error eq 'HASH' + && ($error->{message} // '') eq 'Maximum message size exceeded'; +} + +sub _render_request_too_large { + my ($c, $file) = @_; + + if ($file eq 'rest.cgi') { + return $c->render( + json => { + error => 1, + code => ERROR_UNKNOWN_TRANSIENT, + message => _request_too_large_message(), + documentation => 'https://bmo.readthedocs.io/en/latest/api/', + }, + status => 413 + ); + } + + if ($file eq 'jsonrpc.cgi') { + return $c->render( + json => { + result => undef, + error => { + code => ERROR_UNKNOWN_TRANSIENT, + message => _request_too_large_message(), + }, + id => undef, + }, + status => 413 + ); + } + + if ($file eq 'xmlrpc.cgi') { + my $message = xml_quote(_request_too_large_message()); + my $xml + = qq{\n} + . qq{} + . qq{faultString$message} + . qq{faultCode@{[ERROR_UNKNOWN_TRANSIENT]}} + . qq{\n}; + $c->res->headers->content_type('text/xml; charset=UTF-8'); + return $c->render(data => $xml, status => 413); + } + + return $c->render( + handler => 'bugzilla', + template => 'global/user-error', + format => 'html', + error => 'request_too_large', + status => 413 + ); +} + +sub _request_too_large_message { + my $request_cache = Bugzilla->request_cache; + local $request_cache->{usage_mode} = USAGE_MODE_REST; + local $request_cache->{error_mode} = ERROR_MODE_REST; + + my $message; + my $template = Bugzilla->template; + $template->process( + 'global/user-error.html.tmpl', + {error => 'request_too_large'}, + \$message + ) || die $template->error(); + return trim($message); +} + sub _ENV { my ($c, $script_name) = @_; my $tx = $c->tx; diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index b952e7cedd..80f40cdd36 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -15,11 +15,27 @@ BEGIN { use Bugzilla::Test::MockLocalconfig (urlbase => 'http://bmo.test'); use Bugzilla::Test::MockDB; -use Bugzilla::Test::MockParams (maxattachmentsize => 10_240); +use Bugzilla::Test::MockParams; use Test2::V0; use Test::Mojo; +{ + package TestRequest; + + sub new { + my ($class, $message) = @_; + return bless {message => $message}, $class; + } + + sub error { + my ($self) = @_; + return {message => $self->{message}}; + } + + sub is_limit_exceeded { return 1; } +} + my $boundary = 'bugzilla-request-limit'; my $body = join( "\r\n", @@ -37,26 +53,75 @@ my $body = join( ); my $t = Test::Mojo->new('Bugzilla::App'); +$t->ua->max_response_size(0); + +$t->post_ok('/index.cgi')->status_isnt(413); + $t->post_ok( '/post_bug.cgi' => { 'Content-Length' => length($body), 'Content-Type' => "multipart/form-data; boundary=$boundary", } => $body )->status_is(413) - ->header_is('Content-Type' => 'text/plain; charset=UTF-8') - ->content_is("The request is too large. Attachments are limited to 10 MB.\n") - ->content_unlike(qr/bug_type/i); + ->header_like('Content-Type' => qr{^text/html\b}) + ->content_like(qr{

Request Too Large

}) + ->content_like(qr{The request is too large\.}) + ->content_unlike(qr{

Bug Type Required

}); -my $config = Bugzilla::Config->new; -$config->set_param(maxattachmentsize => 2047); -$config->update; $t->post_ok( - '/post_bug.cgi' => { + '/index.cgi' => { + 'Content-Length' => length($body), + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => $body +)->status_is(413) + ->content_like(qr{The request is too large\.}); + +$t->post_ok( + '/rest/bug/1/attachment' => { 'Content-Length' => length($body), 'Content-Type' => "multipart/form-data; boundary=$boundary", } => $body )->status_is(413) - ->content_is( - "The request is too large. Attachments are limited to 2047 KB.\n"); + ->header_like('Content-Type' => qr{^application/json\b}) + ->json_is('/error' => 1) + ->json_is('/code' => 32_000) + ->json_is('/message' => 'The request is too large.'); + +$t->post_ok( + '/jsonrpc.cgi' => { + 'Content-Length' => length($body), + 'Content-Type' => 'application/json', + } => $body +)->status_is(413) + ->header_like('Content-Type' => qr{^application/json\b}) + ->json_is('/result' => undef) + ->json_is('/error/code' => 32_000) + ->json_is('/error/message' => 'The request is too large.') + ->json_is('/id' => undef); + +$t->post_ok( + '/xmlrpc.cgi' => { + 'Content-Length' => length($body), + 'Content-Type' => 'text/xml', + } => $body +)->status_is(413) + ->header_like('Content-Type' => qr{^text/xml\b}) + ->content_like(qr{}) + ->content_like(qr{faultCode32000}) + ->content_like( + qr{faultStringThe request is too large\.}); + +ok( + Bugzilla::App::Controller::CGI::_is_message_size_exceeded( + TestRequest->new('Maximum message size exceeded') + ), + 'message-size limit errors are rejected' +); +ok( + !Bugzilla::App::Controller::CGI::_is_message_size_exceeded( + TestRequest->new('Maximum header size exceeded') + ), + 'other limit errors retain their existing behavior' +); done_testing; diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index bc8f138738..af82e5db23 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -1720,6 +1720,10 @@ [% ELSIF error == "request_queue_group_invalid" %] The group field [% group FILTER html %] is invalid. + [% ELSIF error == "request_too_large" %] + [% title = "Request Too Large" %] + The request is too large. + [% ELSIF error == "require_new_password" %] [% title = "New Password Needed" %] You cannot change your password without choosing a new one. From 9b9335440ab330207d5f1c8962974b45775b6cb8 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 29 Jul 2026 23:32:33 -0400 Subject: [PATCH 05/12] Bug 1832783 - Register request size user error correctly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 7c376f3c97..ab8e620930 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -28,6 +28,7 @@ use Bugzilla::Util qw(trim xml_quote); use Bugzilla::WebService::Constants qw(ERROR_UNKNOWN_TRANSIENT); my %SEEN; +my $REQUEST_TOO_LARGE_ERROR = 'request_too_large'; sub setup_routes { my ($class, $r) = @_; @@ -162,7 +163,7 @@ sub _render_request_too_large { handler => 'bugzilla', template => 'global/user-error', format => 'html', - error => 'request_too_large', + error => $REQUEST_TOO_LARGE_ERROR, status => 413 ); } @@ -176,7 +177,7 @@ sub _request_too_large_message { my $template = Bugzilla->template; $template->process( 'global/user-error.html.tmpl', - {error => 'request_too_large'}, + {error => $REQUEST_TOO_LARGE_ERROR}, \$message ) || die $template->error(); return trim($message); From 6ab268ab058513b69afa36ef6a352fedbf5d5810 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 1 Aug 2026 12:21:38 -0400 Subject: [PATCH 06/12] Bug 1832783 - Handle CGI request buffer limits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 28 +++++++++++--------- Bugzilla/WebService/Constants.pm | 3 +++ t/app-cgi-request-limit.t | 45 +++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index ab8e620930..683e39ace1 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -19,16 +19,15 @@ use English qw(-no_match_vars); use Bugzilla::App::Stdout; use Bugzilla::Constants qw( bz_locations - ERROR_MODE_REST USAGE_MODE_BROWSER USAGE_MODE_REST ); use Bugzilla::Logging; use Bugzilla::Util qw(trim xml_quote); -use Bugzilla::WebService::Constants qw(ERROR_UNKNOWN_TRANSIENT); +use Bugzilla::WebService::Constants qw(WS_ERROR_CODE); my %SEEN; -my $REQUEST_TOO_LARGE_ERROR = 'request_too_large'; +use constant REQUEST_TOO_LARGE_ERROR => 'request_too_large'; sub setup_routes { my ($class, $r) = @_; @@ -70,7 +69,7 @@ sub load_one { my $wrapper = sub { my ($c) = @_; - if (_is_message_size_exceeded($c->req)) { + if (_is_request_body_limit_exceeded($c->req)) { my $reason = $c->req->error->{message}; WARN("Rejected oversized request for $file: $reason"); return _render_request_too_large($c, $file); @@ -110,22 +109,23 @@ sub load_one { return 1; } -sub _is_message_size_exceeded { +sub _is_request_body_limit_exceeded { my ($request) = @_; my $error = $request->error; return $request->is_limit_exceeded && ref $error eq 'HASH' - && ($error->{message} // '') eq 'Maximum message size exceeded'; + && ($error->{message} // '') =~ /\AMaximum (?:message|buffer) size exceeded\z/; } sub _render_request_too_large { my ($c, $file) = @_; + my $error_code = WS_ERROR_CODE->{REQUEST_TOO_LARGE_ERROR()}; if ($file eq 'rest.cgi') { return $c->render( json => { error => 1, - code => ERROR_UNKNOWN_TRANSIENT, + code => $error_code, message => _request_too_large_message(), documentation => 'https://bmo.readthedocs.io/en/latest/api/', }, @@ -138,7 +138,7 @@ sub _render_request_too_large { json => { result => undef, error => { - code => ERROR_UNKNOWN_TRANSIENT, + code => $error_code, message => _request_too_large_message(), }, id => undef, @@ -153,7 +153,7 @@ sub _render_request_too_large { = qq{\n} . qq{} . qq{faultString$message} - . qq{faultCode@{[ERROR_UNKNOWN_TRANSIENT]}} + . qq{faultCode$error_code} . qq{\n}; $c->res->headers->content_type('text/xml; charset=UTF-8'); return $c->render(data => $xml, status => 413); @@ -163,21 +163,23 @@ sub _render_request_too_large { handler => 'bugzilla', template => 'global/user-error', format => 'html', - error => $REQUEST_TOO_LARGE_ERROR, + error => REQUEST_TOO_LARGE_ERROR, status => 413 ); } sub _request_too_large_message { my $request_cache = Bugzilla->request_cache; - local $request_cache->{usage_mode} = USAGE_MODE_REST; - local $request_cache->{error_mode} = ERROR_MODE_REST; + # Render localized plain text without leaking REST modes into CGI dispatch. + local $request_cache->{usage_mode} = $request_cache->{usage_mode}; + local $request_cache->{error_mode} = $request_cache->{error_mode}; + Bugzilla->usage_mode(USAGE_MODE_REST); my $message; my $template = Bugzilla->template; $template->process( 'global/user-error.html.tmpl', - {error => $REQUEST_TOO_LARGE_ERROR}, + {error => REQUEST_TOO_LARGE_ERROR}, \$message ) || die $template->error(); return trim($message); diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm index 349b2d83dc..5bb9976bc4 100644 --- a/Bugzilla/WebService/Constants.pm +++ b/Bugzilla/WebService/Constants.pm @@ -71,6 +71,7 @@ use constant WS_ERROR_CODE => { number_too_small => 55, illegal_date => 56, illegal_date_pronoun => 57, + request_too_large => 58, # Bug errors usually occupy the 100-200 range. improper_bug_id_field_value => 100, @@ -258,6 +259,7 @@ use constant STATUS_BAD_REQUEST => 400; use constant STATUS_NOT_AUTHORIZED => 401; use constant STATUS_NOT_FOUND => 404; use constant STATUS_GONE => 410; +use constant STATUS_REQUEST_TOO_LARGE => 413; # The integer value is the error code above returned by # the related webvservice call. We choose the appropriate @@ -266,6 +268,7 @@ use constant STATUS_GONE => 410; sub REST_STATUS_CODE_MAP { my $status_code_map = { 51 => STATUS_NOT_FOUND, + 58 => STATUS_REQUEST_TOO_LARGE, 101 => STATUS_NOT_FOUND, 102 => STATUS_NOT_AUTHORIZED, 106 => STATUS_NOT_AUTHORIZED, diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index 80f40cdd36..ec36088082 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -2,6 +2,9 @@ # 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. use strict; use warnings; use 5.10.1; @@ -10,6 +13,7 @@ use lib qw( . lib local/lib/perl5 ); BEGIN { $ENV{BUGZILLA_DISABLE_HOSTAGE} = 1; $ENV{LOG4PERL_CONFIG_FILE} = 'log4perl-t.conf'; + $ENV{MOJO_MAX_BUFFER_SIZE} = 64; $ENV{MOJO_MAX_MESSAGE_SIZE} = 512; } @@ -51,11 +55,34 @@ my $body = join( "--$boundary--", '' ); +my $small_body = join( + "\r\n", + "--$boundary", + 'Content-Disposition: form-data; name="bug_type"', + '', + 'defect', + "--$boundary--", + '' +); my $t = Test::Mojo->new('Bugzilla::App'); +# The request-size environment limit also applies to the client's responses. $t->ua->max_response_size(0); -$t->post_ok('/index.cgi')->status_isnt(413); +$t->post_ok( + '/post_bug.cgi' => { + 'Content-Length' => length($small_body), + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => $small_body +)->status_is(200); + +$t->post_ok( + '/index.cgi' => { + 'Content-Length' => 128, + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => 'x' x 128 +)->status_is(413) + ->content_like(qr{The request is too large\.}); $t->post_ok( '/post_bug.cgi' => { @@ -84,7 +111,7 @@ $t->post_ok( )->status_is(413) ->header_like('Content-Type' => qr{^application/json\b}) ->json_is('/error' => 1) - ->json_is('/code' => 32_000) + ->json_is('/code' => 58) ->json_is('/message' => 'The request is too large.'); $t->post_ok( @@ -95,7 +122,7 @@ $t->post_ok( )->status_is(413) ->header_like('Content-Type' => qr{^application/json\b}) ->json_is('/result' => undef) - ->json_is('/error/code' => 32_000) + ->json_is('/error/code' => 58) ->json_is('/error/message' => 'The request is too large.') ->json_is('/id' => undef); @@ -107,18 +134,24 @@ $t->post_ok( )->status_is(413) ->header_like('Content-Type' => qr{^text/xml\b}) ->content_like(qr{}) - ->content_like(qr{faultCode32000}) + ->content_like(qr{faultCode58}) ->content_like( qr{faultStringThe request is too large\.}); ok( - Bugzilla::App::Controller::CGI::_is_message_size_exceeded( + Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( TestRequest->new('Maximum message size exceeded') ), 'message-size limit errors are rejected' ); ok( - !Bugzilla::App::Controller::CGI::_is_message_size_exceeded( + Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( + TestRequest->new('Maximum buffer size exceeded') + ), + 'buffer-size limit errors are rejected' +); +ok( + !Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( TestRequest->new('Maximum header size exceeded') ), 'other limit errors retain their existing behavior' From 7dd2bee9cab110393cbd21fc957d0e3591b04194 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Sat, 1 Aug 2026 13:26:25 -0400 Subject: [PATCH 07/12] Bug 1832783 - Preserve REST contracts for request limits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 19 +++++++++++++++++-- t/app-cgi-request-limit.t | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 683e39ace1..2d40e1c1f9 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -24,7 +24,7 @@ use Bugzilla::Constants qw( ); use Bugzilla::Logging; use Bugzilla::Util qw(trim xml_quote); -use Bugzilla::WebService::Constants qw(WS_ERROR_CODE); +use Bugzilla::WebService::Constants qw(API_AUTH_HEADERS WS_ERROR_CODE); my %SEEN; use constant REQUEST_TOO_LARGE_ERROR => 'request_too_large'; @@ -121,7 +121,8 @@ sub _render_request_too_large { my ($c, $file) = @_; my $error_code = WS_ERROR_CODE->{REQUEST_TOO_LARGE_ERROR()}; - if ($file eq 'rest.cgi') { + if (path($file)->basename eq 'rest.cgi') { + _set_rest_cors_headers($c); return $c->render( json => { error => 1, @@ -168,6 +169,20 @@ sub _render_request_too_large { ); } +sub _set_rest_cors_headers { + my ($c) = @_; + my @allowed_headers + = qw(accept content-type origin user-agent x-requested-with); + foreach my $header (keys %{API_AUTH_HEADERS()}) { + $header =~ tr/A-Z_/a-z\-/; + push @allowed_headers, $header; + } + + $c->res->headers->header('Access-Control-Allow-Origin' => '*'); + $c->res->headers->header( + 'Access-Control-Allow-Headers' => join(', ', @allowed_headers)); +} + sub _request_too_large_message { my $request_cache = Bugzilla->request_cache; # Render localized plain text without leaking REST modes into CGI dispatch. diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index ec36088082..7e8f513632 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -110,6 +110,25 @@ $t->post_ok( } => $body )->status_is(413) ->header_like('Content-Type' => qr{^application/json\b}) + ->header_is('Access-Control-Allow-Origin' => '*') + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + ) + ->json_is('/error' => 1) + ->json_is('/code' => 58) + ->json_is('/message' => 'The request is too large.'); + +$t->post_ok( + '/bzapi/bug/1/attachment' => { + 'Content-Length' => length($body), + 'Content-Type' => "multipart/form-data; boundary=$boundary", + } => $body +)->status_is(413) + ->header_like('Content-Type' => qr{^application/json\b}) + ->header_is('Access-Control-Allow-Origin' => '*') + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + ) ->json_is('/error' => 1) ->json_is('/code' => 58) ->json_is('/message' => 'The request is too large.'); From 5d2d66e012e2614ac32a4f7359e02dd442ba4538 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Mon, 3 Aug 2026 22:12:19 -0400 Subject: [PATCH 08/12] Bug 1832783 - Reuse shared request error handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/API.pm | 22 ++++------------ Bugzilla/App/Controller/CGI.pm | 41 +++++++----------------------- Bugzilla/App/Plugin/Error.pm | 9 ++++--- Bugzilla/WebService/Server/REST.pm | 15 ++--------- Bugzilla/WebService/Util.pm | 15 +++++++++++ t/app-cgi-request-limit.t | 16 ++++++++++-- 6 files changed, 50 insertions(+), 68 deletions(-) diff --git a/Bugzilla/App/Controller/API.pm b/Bugzilla/App/Controller/API.pm index d1b4c6cc53..bb10974122 100644 --- a/Bugzilla/App/Controller/API.pm +++ b/Bugzilla/App/Controller/API.pm @@ -17,6 +17,7 @@ use Try::Tiny; use Bugzilla::Constants; use Bugzilla::Logging; +use Bugzilla::WebService::Util qw(set_rest_cors_headers); use constant SUPPORTED_VERSIONS => qw(V1); @@ -33,7 +34,7 @@ sub setup_routes { $r->under( '/api' => sub { my ($c) = @_; - _insert_rest_headers($c); + set_rest_cors_headers($c->res->headers); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/user/profile')->to('V1::User#user_profile'); @@ -42,14 +43,14 @@ sub setup_routes { $r->under( '/latest' => sub { my ($c) = @_; - _insert_rest_headers($c); + set_rest_cors_headers($c->res->headers); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/configuration')->to('V1::Configuration#configuration'); $r->under( '/bzapi' => sub { my ($c) = @_; - _insert_rest_headers($c); + set_rest_cors_headers($c->res->headers); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/configuration')->to('V1::Configuration#configuration'); @@ -58,7 +59,7 @@ sub setup_routes { my $rest_routes = $r->under( '/rest' => sub { my ($c) = @_; - _insert_rest_headers($c); + set_rest_cors_headers($c->res->headers); Bugzilla->usage_mode(USAGE_MODE_REST); } ); @@ -103,17 +104,4 @@ sub _load_api_module { }; } -sub _insert_rest_headers { - my ($c) = @_; - - # Access Control - my @allowed_headers - = qw(accept authorization content-type origin user-agent x-bugzilla-api-key x-requested-with); - $c->res->headers->header('Access-Control-Allow-Origin' => '*'); - $c->res->headers->header( - 'Access-Control-Allow-Headers' => join ', ', - @allowed_headers - ); -} - 1; diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 2d40e1c1f9..75511cfe9c 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -20,11 +20,14 @@ use Bugzilla::App::Stdout; use Bugzilla::Constants qw( bz_locations USAGE_MODE_BROWSER + USAGE_MODE_MOJO + USAGE_MODE_MOJO_REST USAGE_MODE_REST ); use Bugzilla::Logging; use Bugzilla::Util qw(trim xml_quote); -use Bugzilla::WebService::Constants qw(API_AUTH_HEADERS WS_ERROR_CODE); +use Bugzilla::WebService::Constants qw(WS_ERROR_CODE); +use Bugzilla::WebService::Util qw(set_rest_cors_headers); my %SEEN; use constant REQUEST_TOO_LARGE_ERROR => 'request_too_large'; @@ -122,16 +125,9 @@ sub _render_request_too_large { my $error_code = WS_ERROR_CODE->{REQUEST_TOO_LARGE_ERROR()}; if (path($file)->basename eq 'rest.cgi') { - _set_rest_cors_headers($c); - return $c->render( - json => { - error => 1, - code => $error_code, - message => _request_too_large_message(), - documentation => 'https://bmo.readthedocs.io/en/latest/api/', - }, - status => 413 - ); + set_rest_cors_headers($c->res->headers); + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + return $c->user_error(REQUEST_TOO_LARGE_ERROR); } if ($file eq 'jsonrpc.cgi') { @@ -160,27 +156,8 @@ sub _render_request_too_large { return $c->render(data => $xml, status => 413); } - return $c->render( - handler => 'bugzilla', - template => 'global/user-error', - format => 'html', - error => REQUEST_TOO_LARGE_ERROR, - status => 413 - ); -} - -sub _set_rest_cors_headers { - my ($c) = @_; - my @allowed_headers - = qw(accept content-type origin user-agent x-requested-with); - foreach my $header (keys %{API_AUTH_HEADERS()}) { - $header =~ tr/A-Z_/a-z\-/; - push @allowed_headers, $header; - } - - $c->res->headers->header('Access-Control-Allow-Origin' => '*'); - $c->res->headers->header( - 'Access-Control-Allow-Headers' => join(', ', @allowed_headers)); + Bugzilla->usage_mode(USAGE_MODE_MOJO); + return $c->user_error(REQUEST_TOO_LARGE_ERROR, {}, {status => 413}); } sub _request_too_large_message { diff --git a/Bugzilla/App/Plugin/Error.pm b/Bugzilla/App/Plugin/Error.pm index 676a2e836e..06e8cd8123 100644 --- a/Bugzilla/App/Plugin/Error.pm +++ b/Bugzilla/App/Plugin/Error.pm @@ -28,7 +28,8 @@ sub register { } sub _render_error { - my ($type, $c, $error, $vars) = @_; + my ($type, $c, $error, $vars, $options) = @_; + $options ||= {}; # If values are defined in the stash, use those instead my $stash = $c->stash; @@ -49,7 +50,7 @@ sub _render_error { if (Bugzilla->usage_mode == USAGE_MODE_MOJO) { $logfunc->("webpage error: $error"); - if ($c->app->mode eq 'development') { + if ($c->app->mode eq 'development' && !exists $options->{status}) { use Bugzilla::Logging; my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception'; my $e = $class->new($error)->trace(2); @@ -63,8 +64,8 @@ sub _render_error { template => "global/$type-error", format => 'html', error => $error, - status => 200, - %{$vars} + %{$vars}, + status => $options->{status} // $vars->{status} // 200 ); return 0; } diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm index 59c4653858..8c1d77ec4c 100644 --- a/Bugzilla/WebService/Server/REST.pm +++ b/Bugzilla/WebService/Server/REST.pm @@ -19,7 +19,7 @@ use Bugzilla::Error; use Bugzilla::Hook; use Bugzilla::Util qw(html_quote disable_utf8 enable_utf8); use Bugzilla::WebService::Constants; -use Bugzilla::WebService::Util qw(taint_data fix_credentials); +use Bugzilla::WebService::Util qw(fix_credentials set_rest_cors_headers taint_data); # Load resource modules use Bugzilla::WebService::Server::REST::Resources::Bug; @@ -141,18 +141,7 @@ sub response { Bugzilla::Hook::process('webservice_rest_response', {rpc => $self, result => \$result, response => $response}); - # Access Control - my @allowed_headers - = qw(accept content-type origin user-agent x-requested-with); - foreach my $header (keys %{API_AUTH_HEADERS()}) { - - # We want to lowercase and replace _ with - - my $translated_header = $header; - $translated_header =~ tr/A-Z_/a-z\-/; - push(@allowed_headers, $translated_header); - } - $response->header("Access-Control-Allow-Origin", "*"); - $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers)); + set_rest_cors_headers($response); # ETag support my $etag = $self->bz_etag; diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index 7e92dc40bf..5fa515e953 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -37,8 +37,23 @@ our @EXPORT_OK = qw( translate params_to_objects fix_credentials + set_rest_cors_headers ); +sub set_rest_cors_headers { + my ($headers) = @_; + my @allowed_headers + = qw(accept authorization content-type origin user-agent x-requested-with); + foreach my $header (sort keys %{API_AUTH_HEADERS()}) { + $header =~ tr/A-Z_/a-z\-/; + push @allowed_headers, $header; + } + + $headers->header('Access-Control-Allow-Origin' => '*'); + $headers->header( + 'Access-Control-Allow-Headers' => join(', ', @allowed_headers)); +} + sub extract_flags { my ($flags, $bug, $attachment) = @_; my (@new_flags, @old_flags); diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index 7e8f513632..eeab1c3dd7 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -112,7 +112,13 @@ $t->post_ok( ->header_like('Content-Type' => qr{^application/json\b}) ->header_is('Access-Control-Allow-Origin' => '*') ->header_like( - 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + 'Access-Control-Allow-Headers' => qr{\bauthorization\b} + ) + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + ) + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-login\b} ) ->json_is('/error' => 1) ->json_is('/code' => 58) @@ -127,7 +133,13 @@ $t->post_ok( ->header_like('Content-Type' => qr{^application/json\b}) ->header_is('Access-Control-Allow-Origin' => '*') ->header_like( - 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + 'Access-Control-Allow-Headers' => qr{\bauthorization\b} + ) + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} + ) + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-login\b} ) ->json_is('/error' => 1) ->json_is('/code' => 58) From aad077c3a1ab618fbac8c8e67f5d2a44f582ee91 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Mon, 3 Aug 2026 22:43:45 -0400 Subject: [PATCH 09/12] Bug 1832783 - Drop removed XML-RPC coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 14 +------------- t/app-cgi-request-limit.t | 12 ------------ 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 75511cfe9c..9f5af7584e 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -25,7 +25,7 @@ use Bugzilla::Constants qw( USAGE_MODE_REST ); use Bugzilla::Logging; -use Bugzilla::Util qw(trim xml_quote); +use Bugzilla::Util qw(trim); use Bugzilla::WebService::Constants qw(WS_ERROR_CODE); use Bugzilla::WebService::Util qw(set_rest_cors_headers); @@ -144,18 +144,6 @@ sub _render_request_too_large { ); } - if ($file eq 'xmlrpc.cgi') { - my $message = xml_quote(_request_too_large_message()); - my $xml - = qq{\n} - . qq{} - . qq{faultString$message} - . qq{faultCode$error_code} - . qq{\n}; - $c->res->headers->content_type('text/xml; charset=UTF-8'); - return $c->render(data => $xml, status => 413); - } - Bugzilla->usage_mode(USAGE_MODE_MOJO); return $c->user_error(REQUEST_TOO_LARGE_ERROR, {}, {status => 413}); } diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index eeab1c3dd7..236f3845ea 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -157,18 +157,6 @@ $t->post_ok( ->json_is('/error/message' => 'The request is too large.') ->json_is('/id' => undef); -$t->post_ok( - '/xmlrpc.cgi' => { - 'Content-Length' => length($body), - 'Content-Type' => 'text/xml', - } => $body -)->status_is(413) - ->header_like('Content-Type' => qr{^text/xml\b}) - ->content_like(qr{}) - ->content_like(qr{faultCode58}) - ->content_like( - qr{faultStringThe request is too large\.}); - ok( Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( TestRequest->new('Maximum message size exceeded') From d590a81e5fe5227ac69b5390fcb75114ae1a1868 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 5 Aug 2026 22:04:52 -0400 Subject: [PATCH 10/12] Bug 1832783 - Address current master review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/API.pm | 15 +++++++++++---- Bugzilla/App/Controller/CGI.pm | 35 ---------------------------------- Bugzilla/WebService/Util.pm | 17 ++++++++++------- t/app-cgi-request-limit.t | 33 -------------------------------- 4 files changed, 21 insertions(+), 79 deletions(-) diff --git a/Bugzilla/App/Controller/API.pm b/Bugzilla/App/Controller/API.pm index bb10974122..d53d6417a0 100644 --- a/Bugzilla/App/Controller/API.pm +++ b/Bugzilla/App/Controller/API.pm @@ -34,7 +34,7 @@ sub setup_routes { $r->under( '/api' => sub { my ($c) = @_; - set_rest_cors_headers($c->res->headers); + _insert_rest_headers($c); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/user/profile')->to('V1::User#user_profile'); @@ -43,14 +43,14 @@ sub setup_routes { $r->under( '/latest' => sub { my ($c) = @_; - set_rest_cors_headers($c->res->headers); + _insert_rest_headers($c); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/configuration')->to('V1::Configuration#configuration'); $r->under( '/bzapi' => sub { my ($c) = @_; - set_rest_cors_headers($c->res->headers); + _insert_rest_headers($c); Bugzilla->usage_mode(USAGE_MODE_REST); } )->get('/configuration')->to('V1::Configuration#configuration'); @@ -59,7 +59,7 @@ sub setup_routes { my $rest_routes = $r->under( '/rest' => sub { my ($c) = @_; - set_rest_cors_headers($c->res->headers); + _insert_rest_headers($c); Bugzilla->usage_mode(USAGE_MODE_REST); } ); @@ -104,4 +104,11 @@ sub _load_api_module { }; } +sub _insert_rest_headers { + my ($c) = @_; + my @allowed_headers + = qw(accept authorization content-type origin user-agent x-bugzilla-api-key x-requested-with); + set_rest_cors_headers($c->res->headers, \@allowed_headers); +} + 1; diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 9f5af7584e..4a48e39177 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -22,11 +22,8 @@ use Bugzilla::Constants qw( USAGE_MODE_BROWSER USAGE_MODE_MOJO USAGE_MODE_MOJO_REST - USAGE_MODE_REST ); use Bugzilla::Logging; -use Bugzilla::Util qw(trim); -use Bugzilla::WebService::Constants qw(WS_ERROR_CODE); use Bugzilla::WebService::Util qw(set_rest_cors_headers); my %SEEN; @@ -122,7 +119,6 @@ sub _is_request_body_limit_exceeded { sub _render_request_too_large { my ($c, $file) = @_; - my $error_code = WS_ERROR_CODE->{REQUEST_TOO_LARGE_ERROR()}; if (path($file)->basename eq 'rest.cgi') { set_rest_cors_headers($c->res->headers); @@ -130,41 +126,10 @@ sub _render_request_too_large { return $c->user_error(REQUEST_TOO_LARGE_ERROR); } - if ($file eq 'jsonrpc.cgi') { - return $c->render( - json => { - result => undef, - error => { - code => $error_code, - message => _request_too_large_message(), - }, - id => undef, - }, - status => 413 - ); - } - Bugzilla->usage_mode(USAGE_MODE_MOJO); return $c->user_error(REQUEST_TOO_LARGE_ERROR, {}, {status => 413}); } -sub _request_too_large_message { - my $request_cache = Bugzilla->request_cache; - # Render localized plain text without leaking REST modes into CGI dispatch. - local $request_cache->{usage_mode} = $request_cache->{usage_mode}; - local $request_cache->{error_mode} = $request_cache->{error_mode}; - Bugzilla->usage_mode(USAGE_MODE_REST); - - my $message; - my $template = Bugzilla->template; - $template->process( - 'global/user-error.html.tmpl', - {error => REQUEST_TOO_LARGE_ERROR}, - \$message - ) || die $template->error(); - return trim($message); -} - sub _ENV { my ($c, $script_name) = @_; my $tx = $c->tx; diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm index 5fa515e953..32b34a4bb0 100644 --- a/Bugzilla/WebService/Util.pm +++ b/Bugzilla/WebService/Util.pm @@ -41,17 +41,20 @@ our @EXPORT_OK = qw( ); sub set_rest_cors_headers { - my ($headers) = @_; - my @allowed_headers - = qw(accept authorization content-type origin user-agent x-requested-with); - foreach my $header (sort keys %{API_AUTH_HEADERS()}) { - $header =~ tr/A-Z_/a-z\-/; - push @allowed_headers, $header; + my ($headers, $allowed_headers) = @_; + if (!$allowed_headers) { + my @default_headers + = qw(accept authorization content-type origin user-agent x-requested-with); + foreach my $header (sort keys %{API_AUTH_HEADERS()}) { + $header =~ tr/A-Z_/a-z\-/; + push @default_headers, $header; + } + $allowed_headers = \@default_headers; } $headers->header('Access-Control-Allow-Origin' => '*'); $headers->header( - 'Access-Control-Allow-Headers' => join(', ', @allowed_headers)); + 'Access-Control-Allow-Headers' => join(', ', @$allowed_headers)); } sub extract_flags { diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index 236f3845ea..3a8977458d 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -124,39 +124,6 @@ $t->post_ok( ->json_is('/code' => 58) ->json_is('/message' => 'The request is too large.'); -$t->post_ok( - '/bzapi/bug/1/attachment' => { - 'Content-Length' => length($body), - 'Content-Type' => "multipart/form-data; boundary=$boundary", - } => $body -)->status_is(413) - ->header_like('Content-Type' => qr{^application/json\b}) - ->header_is('Access-Control-Allow-Origin' => '*') - ->header_like( - 'Access-Control-Allow-Headers' => qr{\bauthorization\b} - ) - ->header_like( - 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-api-key\b} - ) - ->header_like( - 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-login\b} - ) - ->json_is('/error' => 1) - ->json_is('/code' => 58) - ->json_is('/message' => 'The request is too large.'); - -$t->post_ok( - '/jsonrpc.cgi' => { - 'Content-Length' => length($body), - 'Content-Type' => 'application/json', - } => $body -)->status_is(413) - ->header_like('Content-Type' => qr{^application/json\b}) - ->json_is('/result' => undef) - ->json_is('/error/code' => 58) - ->json_is('/error/message' => 'The request is too large.') - ->json_is('/id' => undef); - ok( Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( TestRequest->new('Maximum message size exceeded') From f096c6cad1486e25637610be012139df8f900c5b Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Thu, 6 Aug 2026 21:56:18 -0400 Subject: [PATCH 11/12] Bug 1832783 - Handle all CGI parser limits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/CGI.pm | 15 +++++----- Bugzilla/App/Plugin/Error.pm | 2 +- t/app-cgi-request-limit.t | 54 +++++++++++++++++++++++----------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index 4a48e39177..e0237866e7 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -69,7 +69,7 @@ sub load_one { my $wrapper = sub { my ($c) = @_; - if (_is_request_body_limit_exceeded($c->req)) { + if (_is_request_limit_exceeded($c->req)) { my $reason = $c->req->error->{message}; WARN("Rejected oversized request for $file: $reason"); return _render_request_too_large($c, $file); @@ -109,12 +109,9 @@ sub load_one { return 1; } -sub _is_request_body_limit_exceeded { +sub _is_request_limit_exceeded { my ($request) = @_; - my $error = $request->error; - return $request->is_limit_exceeded - && ref $error eq 'HASH' - && ($error->{message} // '') =~ /\AMaximum (?:message|buffer) size exceeded\z/; + return $request->is_limit_exceeded; } sub _render_request_too_large { @@ -127,7 +124,11 @@ sub _render_request_too_large { } Bugzilla->usage_mode(USAGE_MODE_MOJO); - return $c->user_error(REQUEST_TOO_LARGE_ERROR, {}, {status => 413}); + return $c->user_error( + REQUEST_TOO_LARGE_ERROR, + {}, + {status => 413, skip_exception_page => 1} + ); } sub _ENV { diff --git a/Bugzilla/App/Plugin/Error.pm b/Bugzilla/App/Plugin/Error.pm index 06e8cd8123..f119a4dce1 100644 --- a/Bugzilla/App/Plugin/Error.pm +++ b/Bugzilla/App/Plugin/Error.pm @@ -50,7 +50,7 @@ sub _render_error { if (Bugzilla->usage_mode == USAGE_MODE_MOJO) { $logfunc->("webpage error: $error"); - if ($c->app->mode eq 'development' && !exists $options->{status}) { + if ($c->app->mode eq 'development' && !$options->{skip_exception_page}) { use Bugzilla::Logging; my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception'; my $e = $class->new($error)->trace(2); diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index 3a8977458d..a01b2146c7 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -28,8 +28,11 @@ use Test::Mojo; package TestRequest; sub new { - my ($class, $message) = @_; - return bless {message => $message}, $class; + my ($class, $message, $is_limit_exceeded) = @_; + return bless { + message => $message, + is_limit_exceeded => $is_limit_exceeded // 1 + }, $class; } sub error { @@ -37,7 +40,10 @@ use Test::Mojo; return {message => $self->{message}}; } - sub is_limit_exceeded { return 1; } + sub is_limit_exceeded { + my ($self) = @_; + return $self->{is_limit_exceeded}; + } } my $boundary = 'bugzilla-request-limit'; @@ -124,23 +130,37 @@ $t->post_ok( ->json_is('/code' => 58) ->json_is('/message' => 'The request is too large.'); +for my $reason ( + 'Maximum message size exceeded', + 'Maximum buffer size exceeded', + 'Maximum header size exceeded', + 'Maximum start-line size exceeded', + 'A future Mojolicious limit error' +) { + ok( + Bugzilla::App::Controller::CGI::_is_request_limit_exceeded( + TestRequest->new($reason) + ), + "$reason is rejected" + ); +} + ok( - Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( - TestRequest->new('Maximum message size exceeded') - ), - 'message-size limit errors are rejected' -); -ok( - Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( - TestRequest->new('Maximum buffer size exceeded') + !Bugzilla::App::Controller::CGI::_is_request_limit_exceeded( + TestRequest->new('Unrelated parser error', 0) ), - 'buffer-size limit errors are rejected' + 'non-limit parser errors retain their existing behavior' ); -ok( - !Bugzilla::App::Controller::CGI::_is_request_body_limit_exceeded( - TestRequest->new('Maximum header size exceeded') - ), - 'other limit errors retain their existing behavior' + +$t->app->routes->get('/_test/status-error')->to( + cb => sub { + my ($c) = @_; + Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_MOJO); + return $c->user_error('request_too_large', {}, {status => 413}); + } ); +$t->get_ok('/_test/status-error') + ->status_is(500) + ->content_like(qr{Server Error \(development mode\)}); done_testing; From 0c4f423ca33c83eb8db8adaf1af80fc7d427d0dc Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Fri, 7 Aug 2026 20:29:52 -0400 Subject: [PATCH 12/12] Bug 1832783 - Reject native API parser limits Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Bugzilla/App/Controller/API.pm | 51 ++++++++++++-------------- Bugzilla/App/Controller/CGI.pm | 7 +--- t/app-cgi-request-limit.t | 66 +++++++++++++--------------------- 3 files changed, 48 insertions(+), 76 deletions(-) diff --git a/Bugzilla/App/Controller/API.pm b/Bugzilla/App/Controller/API.pm index d53d6417a0..767a66191d 100644 --- a/Bugzilla/App/Controller/API.pm +++ b/Bugzilla/App/Controller/API.pm @@ -20,6 +20,7 @@ use Bugzilla::Logging; use Bugzilla::WebService::Util qw(set_rest_cors_headers); use constant SUPPORTED_VERSIONS => qw(V1); +use constant REQUEST_TOO_LARGE_ERROR => 'request_too_large'; sub setup_routes { my ($class, $r) = @_; @@ -31,38 +32,17 @@ sub setup_routes { $r->namespaces($namespaces); # Backwards compat with /api/user/profile which Phabricator requires - $r->under( - '/api' => sub { - my ($c) = @_; - _insert_rest_headers($c); - Bugzilla->usage_mode(USAGE_MODE_REST); - } - )->get('/user/profile')->to('V1::User#user_profile'); + $r->under('/api' => \&_prepare_rest_request) + ->get('/user/profile')->to('V1::User#user_profile'); # Other backwards compat routes - $r->under( - '/latest' => sub { - my ($c) = @_; - _insert_rest_headers($c); - Bugzilla->usage_mode(USAGE_MODE_REST); - } - )->get('/configuration')->to('V1::Configuration#configuration'); - $r->under( - '/bzapi' => sub { - my ($c) = @_; - _insert_rest_headers($c); - Bugzilla->usage_mode(USAGE_MODE_REST); - } - )->get('/configuration')->to('V1::Configuration#configuration'); + $r->under('/latest' => \&_prepare_rest_request) + ->get('/configuration')->to('V1::Configuration#configuration'); + $r->under('/bzapi' => \&_prepare_rest_request) + ->get('/configuration')->to('V1::Configuration#configuration'); # Set the usage mode for all routes under /rest - my $rest_routes = $r->under( - '/rest' => sub { - my ($c) = @_; - _insert_rest_headers($c); - Bugzilla->usage_mode(USAGE_MODE_REST); - } - ); + my $rest_routes = $r->under('/rest' => \&_prepare_rest_request); # Standard API support foreach my $version (SUPPORTED_VERSIONS) { @@ -90,6 +70,21 @@ sub setup_routes { } } +sub _prepare_rest_request { + my ($c) = @_; + _insert_rest_headers($c); + + if ($c->req->is_limit_exceeded) { + my $reason = $c->req->error->{message}; + WARN("Rejected oversized request for native REST API: $reason"); + Bugzilla->usage_mode(USAGE_MODE_MOJO_REST); + return $c->user_error(REQUEST_TOO_LARGE_ERROR); + } + + Bugzilla->usage_mode(USAGE_MODE_REST); + return 1; +} + sub _load_api_module { my ($routes, $module) = @_; try { diff --git a/Bugzilla/App/Controller/CGI.pm b/Bugzilla/App/Controller/CGI.pm index e0237866e7..81ca94f26c 100644 --- a/Bugzilla/App/Controller/CGI.pm +++ b/Bugzilla/App/Controller/CGI.pm @@ -69,7 +69,7 @@ sub load_one { my $wrapper = sub { my ($c) = @_; - if (_is_request_limit_exceeded($c->req)) { + if ($c->req->is_limit_exceeded) { my $reason = $c->req->error->{message}; WARN("Rejected oversized request for $file: $reason"); return _render_request_too_large($c, $file); @@ -109,11 +109,6 @@ sub load_one { return 1; } -sub _is_request_limit_exceeded { - my ($request) = @_; - return $request->is_limit_exceeded; -} - sub _render_request_too_large { my ($c, $file) = @_; diff --git a/t/app-cgi-request-limit.t b/t/app-cgi-request-limit.t index a01b2146c7..8fe5fdd720 100644 --- a/t/app-cgi-request-limit.t +++ b/t/app-cgi-request-limit.t @@ -24,28 +24,6 @@ use Bugzilla::Test::MockParams; use Test2::V0; use Test::Mojo; -{ - package TestRequest; - - sub new { - my ($class, $message, $is_limit_exceeded) = @_; - return bless { - message => $message, - is_limit_exceeded => $is_limit_exceeded // 1 - }, $class; - } - - sub error { - my ($self) = @_; - return {message => $self->{message}}; - } - - sub is_limit_exceeded { - my ($self) = @_; - return $self->{is_limit_exceeded}; - } -} - my $boundary = 'bugzilla-request-limit'; my $body = join( "\r\n", @@ -130,27 +108,31 @@ $t->post_ok( ->json_is('/code' => 58) ->json_is('/message' => 'The request is too large.'); -for my $reason ( - 'Maximum message size exceeded', - 'Maximum buffer size exceeded', - 'Maximum header size exceeded', - 'Maximum start-line size exceeded', - 'A future Mojolicious limit error' -) { - ok( - Bugzilla::App::Controller::CGI::_is_request_limit_exceeded( - TestRequest->new($reason) - ), - "$reason is rejected" - ); -} +$t->post_ok( + '/rest/component/Test' => { + 'Content-Length' => 2, + 'Content-Type' => 'application/json', + } => '{}' +)->status_is(401) + ->json_is('/code' => 410); -ok( - !Bugzilla::App::Controller::CGI::_is_request_limit_exceeded( - TestRequest->new('Unrelated parser error', 0) - ), - 'non-limit parser errors retain their existing behavior' -); +$t->post_ok( + '/rest/component/Test' => { + 'Content-Length' => length($body), + 'Content-Type' => 'application/json', + } => $body +)->status_is(413) + ->header_like('Content-Type' => qr{^application/json\b}) + ->header_is('Access-Control-Allow-Origin' => '*') + ->header_like( + 'Access-Control-Allow-Headers' => qr{\bauthorization\b} + ) + ->header_unlike( + 'Access-Control-Allow-Headers' => qr{\bx-bugzilla-login\b} + ) + ->json_is('/error' => 1) + ->json_is('/code' => 58) + ->json_is('/message' => 'The request is too large.'); $t->app->routes->get('/_test/status-error')->to( cb => sub {