diff --git a/config/feature_flags/gitlab_com_derisk/allow_nokogiri_parse_huge_xml.yml b/config/feature_flags/gitlab_com_derisk/allow_nokogiri_parse_huge_xml.yml
new file mode 100644
index 0000000000000000000000000000000000000000..00034cefcd7a694081a07391064153f4ad8634d2
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/allow_nokogiri_parse_huge_xml.yml
@@ -0,0 +1,8 @@
+---
+name: allow_nokogiri_parse_huge_xml
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148779
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/454797
+milestone: '17.0'
+group: group::pipeline execution
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 04481b10b3949a24eff395431f8f98b62f9c2b04..fc91bff0e181b11881ca97f949ffb159c4a85dc8 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -10,8 +10,12 @@ class Junit
 
           def parse!(xml_data, test_report, job:)
             test_suite = test_report.get_suite(job.test_suite_name)
+            root = if Feature.enabled?(:allow_nokogiri_parse_huge_xml, job.project)
+                     XmlConverter.new(xml_data).to_h
+                   else
+                     Hash.from_xml(xml_data)
+                   end
 
-            root = Hash.from_xml(xml_data)
             total_parsed = 0
             max_test_cases = job.max_test_cases_per_report
 
diff --git a/lib/gitlab/xml_converter.rb b/lib/gitlab/xml_converter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c089ed13426e28995fc0395ba27c853ca12fe62
--- /dev/null
+++ b/lib/gitlab/xml_converter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# rubocop:disable Gitlab/NamespacedClass -- Base method live in the global namespace
+module Gitlab
+  MAX_XML_SIZE = 30.megabytes
+
+  class XmlConverter < ActiveSupport::XMLConverter
+    # Override the default Nokogiri parser in to allow parsing huge XML files
+    def initialize(xml, disallowed_types = nil)
+      if xml.size > MAX_XML_SIZE
+        raise ArgumentError, format(_("The XML file must be less than %{max_size} MB."),
+          max_size: MAX_XML_SIZE / 1.megabyte)
+      end
+
+      doc = Nokogiri::XML(xml, &:huge)
+      raise doc.errors.first unless doc.errors.empty?
+
+      # These two variables are internally required by `ActiveSupport::XMLConverter`
+      @xml = normalize_keys(doc.to_hash)
+      @disallowed_types = disallowed_types || DISALLOWED_TYPES
+    end
+  end
+end
+# rubocop:enable Gitlab/NamespacedClass
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 29eb0a159b5f89517da017277e86be197a6c2bb9..9762cbfffc5349fcd89df0aaaf59bb418453af30 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -51402,6 +51402,9 @@ msgstr ""
 msgid "The URLs for connecting to Elasticsearch. For clustering, add the URLs separated by commas."
 msgstr ""
 
+msgid "The XML file must be less than %{max_size} MB."
+msgstr ""
+
 msgid "The `/merge` quick action requires the SHA of the head of the branch."
 msgstr ""
 
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 64db92a1d890be9c48783bb5f375eccb55142a6d..77a693f1d0da627f85d5d7e21c4d42bb9da1ef59 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -1,17 +1,19 @@
 # frozen_string_literal: true
 
-require 'fast_spec_helper'
+require 'spec_helper'
 
 RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
   describe '#parse!' do
     subject { described_class.new.parse!(junit, test_report, job: job) }
 
-    let(:job) { double(test_suite_name: 'rspec', max_test_cases_per_report: max_test_cases) }
+    let(:project) { build(:project) }
+    let(:job) { double(test_suite_name: 'rspec', max_test_cases_per_report: max_test_cases, project: project) }
 
     let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
     let(:test_suite) { test_report.get_suite(job.test_suite_name) }
     let(:test_cases) { flattened_test_cases(test_suite) }
     let(:max_test_cases) { 0 }
+    let(:junit) { '<testsuites></testsuites>' }
 
     context 'when data is JUnit style XML' do
       context 'when there are no <testcases> in <testsuite>' do
@@ -379,7 +381,7 @@
     end
 
     context 'when data is not XML' do
-      let(:junit) { double(:random_trash) }
+      let(:junit) { double(:random_trash, size: 1) }
 
       it 'attaches an error to the TestSuite object' do
         expect { subject }.not_to raise_error
@@ -521,6 +523,26 @@
       end
     end
 
+    it 'parses XML using XmlConverter instead of Hash.from_xml' do
+      expect(Gitlab::XmlConverter).to receive(:new).with(junit).once.and_call_original
+      expect(Hash).not_to receive(:from_xml)
+
+      subject
+    end
+
+    context 'when feature flag `allow_nokogiri_parse_huge_xml` is disabled' do
+      before do
+        stub_feature_flags(allow_nokogiri_parse_huge_xml: false)
+      end
+
+      it 'parses XML using Hash.from_xml instead of XmlConverter' do
+        expect(Gitlab::XmlConverter).not_to receive(:new)
+        expect(Hash).to receive(:from_xml).with(junit).once.and_call_original
+
+        subject
+      end
+    end
+
     private
 
     def flattened_test_cases(test_suite)
diff --git a/spec/lib/gitlab/xml_converter_spec.rb b/spec/lib/gitlab/xml_converter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..de5130127580dee058d1ac670abeb8613c4d6a79
--- /dev/null
+++ b/spec/lib/gitlab/xml_converter_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::XmlConverter, feature_category: :pipeline_composition do
+  describe '#to_h' do
+    let(:xml_data) { '<root><key>value</key></root>' }
+
+    subject(:to_h) { described_class.new(xml_data).to_h }
+
+    context "when the xml is valid" do
+      let(:xml_data) { '<root><key>value</key></root>' }
+
+      it "parses the xml with huge option" do
+        expect(Nokogiri).to receive(:XML).and_wrap_original do |original_method, *args, &block|
+          expect(block).to be(Proc.new(&:huge))
+          original_method.call(*args, &block)
+        end
+
+        expect(to_h).to eq('root' => { 'key' => 'value' })
+      end
+    end
+
+    context "when the xml is invalid" do
+      let(:xml_data) { '<root><key>value</key>' }
+
+      it "raises an error" do
+        expect { to_h }.to raise_error(Nokogiri::XML::SyntaxError)
+      end
+    end
+
+    context "when the xml is too large" do
+      let(:xml_data) { instance_double(String, size: Gitlab::MAX_XML_SIZE + 1) }
+
+      it "raises an error" do
+        expect { to_h }.to raise_error(ArgumentError, "The XML file must be less than 30 MB.")
+      end
+    end
+  end
+end