Wrong http_code in modsec logs when using auth_request
Problem
When auth_request directive is active, ModSecurity-nginx captures the HTTP status code
from the auth_request subrequest (usually 200 for "pass") instead of the final status
code from the actual backend (proxy_pass).
Setup
location /auth {
internal;
modsecurity off;
proxy_pass http://127.0.0.1:2607/auth?server=$server_name; # => returns 200
proxy_cache off;
proxy_pass_request_body off;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_intercept_errors off;
}
auth_request /auth;
proxy_pass $upstream; # => returns 404
Scenario
- Request comes in: GET /?test=/bin/bash
- auth_request /auth → returns 200 (pass)
- ModSecurity captures: http_code = 200
- proxy_pass $upstream → returns 404
- Audit log has: http_code: 200 ❌ (should be 404)
Expected Behavior
ModSecurity should capture the FINAL status code sent to the client, not intermediate subrequest statuses.
EDIT
The whole response content in the log seems to be the reponse datas of the /auth subrequest, not the proxy request.
nginx version: nginx/1.29.3
ModSecurity-nginx: v1.0.4-2-gfd28e6a
Hello,
if somebody needs help to reproduce the case (or fix my use case), please ping me :)
I really need the corret http_code in the logs for better rule fixing.
Hi @jaysee,
I apologize that missed out this issue - I try to take a look at this soon.
Anyway, an example to reproduce the issue with the config you given would be helpful.
Here is a minimal setup you can try:
server {
server_name nginx;
listen nginx:80;
# basically CRS setup
include snippets/security.conf;
location /auth {
internal;
modsecurity off;
return 200;
}
location / {
auth_request /auth;
proxy_pass http://test:80;
}
access_log /var/log/nginx/test.log vhost;
}
Calling curl http://nginx/404 -I
HTTP/1.1 404 Not Found
Date: Thu, 27 Nov 2025 15:45:35 GMT
Content-Type: text/html; charset=iso-8859-1
Connection: keep-alive
Vary: Accept-Encoding
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
but modsec log says "http_code": 200
@jaysee
thanks for detailed info - sorry to ask, but in which log you get 200?
It seems that your request is a regular one without any violation, which would a good reason to trigger a rule. If no rule triggers during the transaction, then you won't get anything in any log.
Now I tried your setup, I also get a 404, but all of my logs (error.log, audit.log) are empty.
Sorry, incomplet configuration in my test config
log all, increase anomaly score
modsecurity_rules "
SecAuditEngine On
SecAction \"id:900112,phase:1,pass,nolog,setvar:tx.inbound_anomaly_score_threshold=80,setvar:tx.outbound_anomaly_score_threshold=5\"
";
curl -I "http://nginx/404?t=phpinfo"
HTTP/1.1 404 Not Found
Date: Mon, 01 Dec 2025 06:28:57 GMT
Content-Type: text/html; charset=iso-8859-1
Connection: keep-alive
Vary: Accept-Encoding
Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
{
"transaction": {
"client_ip": "10.0.9.96",
"time_stamp": "Mon Dec 1 07:23:31 2025",
"server_id": "xxx",
"client_port": 37674,
"host_ip": "10.0.9.96",
"host_port": 80,
"unique_id": "yyy",
"request": {
"method": "HEAD",
"http_version": 1.1,
"uri": "/404?t=phpinfo",
"headers": {
"Host": "nginx",
"User-Agent": "curl/8.14.1",
"Accept": "*/*"
}
},
"response": {
"http_code": 200,
"headers": {
"Server": "",
"Date": "Mon, 01 Dec 2025 06:23:31 GMT",
"Content-Type": "application/octet-stream",
"X-Content-Type-Options": "nosniff",
"Connection": "close",
"client_body_buffer_size": "",
"64M": "",
"X-Frame-Options": "SAMEORIGIN",
"Content-Security-Policy": "frame-ancestors 'self'",
"X-Powered-By": ""
}
},
"producer": {
"modsecurity": "ModSecurity v3.0.14 (Linux)",
"connector": "ModSecurity-nginx v1.0.4",
"secrules_engine": "Enabled",
"components": [
"OWASP_CRS/4.21.0-dev\""
]
},
"messages": [
{
"message": "PHP Injection Attack: High-Risk PHP Function Name Found",
"details": {
"match": "Matched \"Operator `PmFromFile' with parameter `php-function-names-933150.data' against variable `ARGS:t' (Value: `phpinfo' )",
"reference": "o0,7v12,7",
"ruleId": "933150",
"file": "/etc/nginx/modsec/crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf",
"lineNumber": "320",
"data": "Matched Data: phpinfo found within ARGS:t: phpinfo",
"severity": "2",
"ver": "OWASP_CRS/4.21.0-dev",
"rev": "",
"tags": [
"application-multi",
"language-php",
"platform-multi",
"attack-injection-php",
"paranoia-level/1",
"OWASP_CRS",
"OWASP_CRS/ATTACK-PHP",
"capec/1000/152/242"
],
"maturity": "0",
"accuracy": "0"
}
}
]
}
}
Sorry, unfortunately I don't see exactly what do you want to achive and what configuration you use.
Could you paste here your full vhost config, and modsecurity.conf relevant lines (skip the comments)?
The goal is to be able to reproduce the explained behavior.
I tried to rebuild the configuration based on given information above, and first time I got a different result - the http_code values was 404, but when I compared the other values, realized that I used an old module version (1.0.3). I upgraded my module, and I got the same result as you described.
Probably was some change between 1.0.3 and 1.0.4 which modified the module's behavior.
If I had to bet, I would choose this PR.
Could you confirm that with 1.0.3 you get the expected code in the audit.log?
OK, had to fix compilation of 1.0.3 against my system, but, yep, got 404 in log now!!!
{
"transaction": {
"client_ip": "10.0.9.96",
"time_stamp": "Tue Dec 2 08:52:04 2025",
"server_id": "xxx",
"client_port": 39360,
"host_ip": "10.0.9.96",
"host_port": 80,
"unique_id": "yyy",
"request": {
"method": "HEAD",
"http_version": 1.1,
"uri": "/404?t=phpinfo",
"headers": {
"Host": "nginx",
"User-Agent": "curl/8.14.1",
"Accept": "*/*"
}
},
"response": {
"http_code": 404,
"headers": {
"Server": "",
"Date": "Tue, 02 Dec 2025 07:52:04 GMT",
"Content-Type": "text/html; charset=iso-8859-1",
"X-Content-Type-Options": "nosniff",
"Connection": "keep-alive",
"client_body_buffer_size": "",
"64M": "",
"X-Frame-Options": "SAMEORIGIN",
"Content-Security-Policy": "frame-ancestors 'self'",
"X-Powered-By": ""
}
},
"producer": {
"modsecurity": "ModSecurity v3.0.14 (Linux)",
"connector": "ModSecurity-nginx v1.0.3",
"secrules_engine": "Enabled",
"components": [
"OWASP_CRS/4.21.0-dev\""
]
},
"messages": [
{
"message": "PHP Injection Attack: High-Risk PHP Function Name Found",
"details": {
"match": "Matched \"Operator `PmFromFile' with parameter `php-function-names-933150.data' against variable `ARGS:t' (Value: `phpinfo' )",
"reference": "o0,7v12,7",
"ruleId": "933150",
"file": "/etc/nginx/modsec/crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf",
"lineNumber": "320",
"data": "Matched Data: phpinfo found within ARGS:t: phpinfo",
"severity": "2",
"ver": "OWASP_CRS/4.21.0-dev",
"rev": "",
"tags": [
"application-multi",
"language-php",
"platform-multi",
"attack-injection-php",
"paranoia-level/1",
"OWASP_CRS",
"OWASP_CRS/ATTACK-PHP",
"capec/1000/152/242"
],
"maturity": "0",
"accuracy": "0"
}
}
]
}
}
I think this is an interesting situation, because - as I understand - you use an internal redirect.
And the mentioned PR above tried to fix that explained unexpected behavior.
I removed the location /auth block from the vhost context that you showed, so without any internal redirection everything works as you expected.
With that block you always returning with 200, so I risk that the behavior in 1.0.4 is the correct, not the 1.0.3's.
yes: without the auth_request /auth; 1.0.4 logs the code returned by the proxy_pass directive.
actually, the auth_request makes an internal validation and should return 200 if OK, 403/401 if not. If auth_request says 200, then proxy_pass handles the request.
and this is this request status I want to see in logs.
In my real use case /auth is not always returning 200 of course. but that's not the point here.
Here we have 2 subrequests auth_request and proxy_pass which one should be handled... I think this should be the code that is finaly returned to the client? (same about the whole response entry in the log)
Here we have 2 subrequests
auth_requestandproxy_passwhich one should be handled... I think this should be the code that is finaly returned to the client? (same about the wholeresponseentry in the log)
I'm sorry but I don't really get it what you mean here. Could you explain your aim with a few examples?
example as explained in previous comment :
curl -I "http://nginx/404?t=phpinfo" => clients get status=404
auditlog says: "http_code": 200,
I need the same code in auditlog that the client gets as I'm writing a ML engine that eats the modsec logs to target bad bahavior/false positives. "Final" http_code is a good indicator.
If you think the actual behavior is the rigth one (because you know the internal design) and this is not a bug, I will try another nginx configuration that could achieve my goal, no problem.