Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

False positive when charset=utf-8\x0d\x0a in content-type header #3046

Closed
chladic opened this issue Dec 9, 2022 · 40 comments
Closed

False positive when charset=utf-8\x0d\x0a in content-type header #3046

chladic opened this issue Dec 9, 2022 · 40 comments
Assignees
Labels
🐛 bug Something isn't working ⏳ awaiting feedback CRS dev asked feedback ⚠️ ModSec Issue related to ModSecurity

Comments

@chladic
Copy link

chladic commented Dec 9, 2022

Hello CRS team,

some actions in my mobile application are triggering this rule and block request. I didnt set in APP charset=utf-8\x0d\x0a,
so maybe its coming from android.
When I exclude this rule SecRuleRemoveById 922110 then its fine, but I want to exclude it for everything
and could not figure out any exception:

I tried this:
tx.allowed_request_content_type_charset=|utf-8| |utf-8\x0d\x0a| |iso-8859-1| |iso-8859-15| |windows-1252|'

and also this:

SecRule REQUEST_HEADERS:Content-Type "text/plain; charset=utf-8\x0d\x0a" \
    "phase:1,nolog,pass,id:6,t:none,ctl:ruleRemoveById=922110"

I checked 922110 rule and it cant match above with regex defined there.
Anyone can help me to understand this issue please ?

Many thanks

ModSecurity: Warning. Matched "Operator `Rx' with parameter `^(?:(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+)\/(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+))(?:\s*+;\s*+(?:(?:charset\s*+=\s*+(?:\"?(?:iso-8859-15?|windows-1252|utf-8)\b\"?))|(?:(?:c(?:h(?:a(?:r(?:s(?:e[^t\"(),\/:;<=> (714 characters omitted)' against variable `TX:1' (Value: `text/plain; charset=utf-8\x0d\x0a' ) [file "/usr/local/coreruleset-3.3.4/rules/REQUEST-922-MULTIPART-ATTACK.conf"] [line "51"] [id "922110"] [rev ""] [msg "Illegal MIME Multipart Header content-type: charset parameter"] [data "Matched Data: text/plain; charset=utf-8\x0d\x0a found within Content-Type multipart form"] [severity "2"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/272/220"] [tag "paranoia-level/1"] [hostname "10.151.0.2"] [uri "/something.json"] [unique_id "167059009818.061175"] [ref "o0,41o14,27v974,41t:lowercaset:lowercase"],

  • CRS version: 3.3.4
  • Paranoia level setting:1
  • ModSecurity version: 3.0.8
  • Web Server and version: nginx
  • Operating System and version: alpine
@fzipi fzipi self-assigned this Dec 9, 2022
@fzipi
Copy link
Member

fzipi commented Dec 9, 2022

Sadly there is no easy way to have the same allowed_request_content_type_charset in the config and in the rule. The content you see is coming from this include file.

In the end, looks like you are receiving an extra \r\n there, right?

CC @theseion

@chladic
Copy link
Author

chladic commented Dec 9, 2022

When APP calls PATCH with some data inside then its fine, but when it POST empty data to endpoint {}, this rule gets triggered and I cant figure out why there are 2 extra characters \r\n
@fzipi thanks, for a moment i was not sure if problem is text/plain what was removed in 3.3.4 from default allowed content-type or those 2 extra characters

@theseion
Copy link
Contributor

It definitely looks like an illegal request to me. \r\n is the separation token for headers, so if it show up in the value then there are either more than there should be or they have been badly encoded. Do you have the ability to inspect the HTTP traffic, e.g. with tcpdump or Wireshark?

@RedXanadu
Copy link
Member

+1 to what @theseion said. It would be interesting to compare one of your working PATCH requests to one of the broken POST requests in Wireshark or something similar.

@chladic
Copy link
Author

chladic commented Dec 12, 2022

@theseion working on it, hopefully will get data soon

@chladic
Copy link
Author

chladic commented Dec 13, 2022

So this is request what pass:

PATCH /custom.json HTTP/1.1
X-Forwarded-For: 1.1.1.1
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Host: example.com
X-Amzn-Trace-Id: Root=1-111111
Content-Length: 233
user-agent: user-agent
accept-encoding: gzip
authorization: Bearer *******
content-type: multipart/form-data; boundary=--dio-boundary-2382587621
custom-version: 1.1.1
custom-locale: en

----dio-boundary-2382587621
content-disposition: form-data; name="data[type]"

user
----dio-boundary-2382587621
content-disposition: form-data; name="data[attributes][frequency]"

DAILY
----dio-boundary-2382587621--

this trigger modsec rule:

PATCH /custom.json HTTP/1.1                                                                                                                                                                                                                                                                                                                          X-Forwarded-For: 1.1.1.1                                                                                                                                                                                                                                                                                                                                 X-Forwarded-Proto: https
X-Forwarded-Port: 443
Host: example.com
X-Amzn-Trace-Id: Root=1-11111
Content-Length: 303
user-agent: user-agent
accept-encoding: gzip
authorization: Bearer ******
content-type: multipart/form-data; boundary=--dio-boundary-1533285540
custom-version: 1.1.1
custom-locale: en

----dio-boundary-1533285540
content-disposition: form-data; name="data[type]"

user
----dio-boundary-1533285540
content-disposition: form-data; name="data[attributes][frequency]"
content-type: text/plain; charset=utf-8
content-transfer-encoding: binary


----dio-boundary-1533285540--

In second PATCH there is no string DAILY but only null as we dont want any value there. It looks like this cause adding 2 extra headers at the end what modsec does not like or it considers as form data ?

@theseion
Copy link
Contributor

Interesting! It looks like this could be a bug in ModSecurity. ModSecurity has a dedicated multipart parser that does some magic to detect / handle parts with null termination (https://github.com/SpiderLabs/ModSecurity/blob/v3/master/src/request_body_processor/multipart.cc). I wasn't able to spot the issue while looking at the code though. @airween, this might be something for you. null is definitely allowed as content of a part as per RFC.

@chladic
Copy link
Author

chladic commented Dec 14, 2022

Thank you @theseion. Happy to hear its not our application issue. Would you have some smarter idea how to temporarily exclude it without removing whole rule with SecRuleRemoveById 922110 ?

@theseion
Copy link
Contributor

You could write a new rule that looks for utf-8\x0d\x0a in multipart requests and then dynamically disable the rule with ctl:ruleRemoveById=922110 (https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#ctl). It's not perfect but should create a huge hole.

@theseion theseion added ⚠️ ModSec Issue related to ModSecurity 🐛 bug Something isn't working labels Dec 15, 2022
@chladic
Copy link
Author

chladic commented Dec 15, 2022

@theseion sorry to bother you again, but when I write down this exception it does not work:

SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*:\s*text.*" \
        "phase:1,nolog,pass,id:6,ctl:ruleRemoveById=922110"

is this what you meant ? Similar condition is used by 922110 Rule, so I dont understand why it does not match in exception

@theseion
Copy link
Contributor

theseion commented Dec 16, 2022

Your new rule must be processed before the rule you want to remove (see linked documentation). Is that the case? That means it has to run in the same phase or an earlier one and it needs to come before the other rule physically (in the same file or in a file that is read before 922-...). The regular expression looks correct.

BTW, to be safe I would match the header without case sensitivity, e.g. (?i)^content-type\s*:\s*text.* (922110 uses t:lowercase).

If you have the option, you can enable debug logging and you will see when and how your new rule is processed.

@chladic
Copy link
Author

chladic commented Dec 16, 2022

@theseion thanks a lot for a hint. I've been all the time using phase 1, but in this case it has to be phase:2 as it is already part of the body. Now exception works well :)

@theseion
Copy link
Contributor

Great to hear.

@airween
Copy link
Contributor

airween commented Dec 19, 2022

Sorry for the late answer, I could check this issue just now.

So as I see the first comment isn't the issue, that's the correct behavior, right?

@chladic's 4th comment contains two requests, but I couldn't reproduce the request which triggers rule 922210.

Note, that in the second request's first line is a very long line, could you check that, is it correct?

Also to @chladic: could you give us a curl request with we can reproduce the problematic request?

@chladic
Copy link
Author

chladic commented Dec 20, 2022

Hi @airween. This very long line was probably caused by copy/paste issue. Didn't notice, sorry.

What we got from Postman is such a curl:

curl --location --request PATCH 'http://localhost:3000/custom' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--data-raw '{
    "data": {
        "type": "user",
        "attributes": {
            "frequency": ""
        }
    }
}'

or

curl --location --request PATCH 'http://localhost:3000/custom' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--form 'frequency=""'

So whenever we put in frequency some string, it will pass. In case of empty string as in example, rule 922210 gets triggered.

@theseion
Copy link
Contributor

Thanks @chladic. You said above that the request contained null, but I don't see that in the curl you posted.

@chladic
Copy link
Author

chladic commented Dec 20, 2022

@theseion before I took curl from postman I was not aware either, just assumed its null. But in this case it's empty string

@theseion
Copy link
Contributor

Thanks for the clarification.

@airween
Copy link
Contributor

airween commented Dec 20, 2022

@chladic, thanks for the examples.

Unfortunately I still can't reproduce the behavior what you mentioned.

See my log lines:

2022/12/20 15:57:11 [info] 59340#59340: *1 ModSecurity: Warning. Matched "Operator `ValidateByteRange' with parameter `38,44-46,48-58,61,65-90,95,97-122' against variable `REQUEST_BODY' (Value: `{"data":{"type":"user","attributes":{"frequency":}}}' ) [file "/home/airween/src/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1540"] [id "920273"] [rev ""] [msg "Invalid character in request (outside of very strict set)"] [data "REQUEST_BODY={"data":{"type":"user","attributes":{"frequency":}}""}"] [severity "2"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "paranoia-level/4"] [hostname "::1"] [uri "/dump.php"] [unique_id "167154823110.316146"] [ref "o0,1o1,1o6,1o8,1o9,1o14,1o16,1o21,1o23,1o34,1o36,1o37,1o47,1o49,1o50,1o51,1o52,1o53,1v154,54t:urlDecodeUni"], client: ::1, server: _, request: "PATCH /dump.php HTTP/1.1", host: "localhost:8080"

2022/12/20 15:57:33 [info] 59340#59340: *2 ModSecurity: Warning. Matched "Operator `ValidateByteRange' with parameter `38,44-46,48-58,61,65-90,95,97-122' against variable `REQUEST_BODY' (Value: `{"data":{"type":"user","attributes":{"frequency":""}}}' ) [file "/home/airween/src/coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf"] [line "1540"] [id "920273"] [rev ""] [msg "Invalid character in request (outside of very strict set)"] [data "REQUEST_BODY={"data":{"type":"user","attributes":{"frequency":""}}""}"] [severity "2"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/1000/210/272"] [tag "paranoia-level/4"] [hostname "::1"] [uri "/dump.php"] [unique_id "167154825358.051311"] [ref "o0,1o1,1o6,1o8,1o9,1o14,1o16,1o21,1o23,1o34,1o36,1o37,1o47,1o49,1o50,1o51,1o52,1o53,1o54,1o55,1v154,56t:urlDecodeUni"], client: ::1, server: _, request: "PATCH /dump.php HTTP/1.1", host: "localhost:8080"

As you can see, I tried with two requests:

curl --location --request PATCH 'http://localhost:8080/dump.php' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--data-raw '{"data":{"type":"user","attributes":{"frequency":}}}'

and

curl --location --request PATCH 'http://localhost:8080/dump.php' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer ' \
--data-raw '{"data":{"type":"user","attributes":{"frequency":""}}}'

The only difference is between the two requests that in the second case I sent an empty string, while in first case I sent a "none" value.

The triggered rule is the same in both cases: 920273, and the cause is a well-known libmodsecurity3 bug.

Could you attach your relevant log lines after you sent those curl requests above?

@airween
Copy link
Contributor

airween commented Jan 7, 2023

@chladic is it justified to keep this issue open? Is there anything else we can help with?

@theseion theseion added the ⏳ awaiting feedback CRS dev asked feedback label Jan 8, 2023
@chladic
Copy link
Author

chladic commented Jan 11, 2023

@airween Ervin sorry for late reply, Im back on this issue. When doing curl Im also not able to reproduce this. When doing it from mobile application then Im triggering rule 922110. This curl is export from postman. But whenever it is executed from mobile Dio as http client is adding there some other stuff what curl never does.

----dio-boundary-1533285540
content-disposition: form-data; name="data[attributes][frequency]"
content-type: text/plain; charset=utf-8
content-transfer-encoding: binary

@mikegoatly
Copy link

For what its worth, I get this error when a simple multipart form is posted with a text document in it. I can't give you a curl that will reproduce the issue, but this is a mildly redacted capture of a request that's triggering the rule. (Apologies if this is unrelated, especially if this is a scenario that the rule is meant to capture!)

POST https://REDACTED HTTP/1.1
Host: REDACTED
Connection: keep-alive
Content-Length: 410
sec-ch-ua: "Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"
sec-ch-ua-mobile: ?0
authorization: Bearer REDACTED
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.55
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary1WIwjBFZYUeBMZMU
accept: application/json
sec-ch-ua-platform: "Windows"
Origin: https://REDACTED
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: REDACTED

------WebKitFormBoundary1WIwjBFZYUeBMZMU
Content-Disposition: form-data; name="file"; filename="New Text Document.txt"
Content-Type: text/plain

Some sample text

------WebKitFormBoundary1WIwjBFZYUeBMZMU
Content-Disposition: form-data; name="title"

Test
------WebKitFormBoundary1WIwjBFZYUeBMZMU
Content-Disposition: form-data; name="id"

12345
------WebKitFormBoundary1WIwjBFZYUeBMZMU--

@theseion
Copy link
Contributor

Hi @mikegoatly. Please open a separate issue for your question. It would also be very helpful if you could provide log output from ModSecurity. I don't immediately see why your request would be blocked, the log would tell us that.

@mikegoatly
Copy link

mikegoatly commented Jan 24, 2023

Hi @theseion Sorry, very happy to raise a separate issue - the reason I mentioned it here is that the ModSecurity log output is exactly same as this issue:

ModSecurity: Warning. Matched "Operator `Rx' with parameter `^(?:(?:\*|[^\"(),\/: <=>?![\x5c\]{}]+)\/(?:\*|[^\"(),\/: <=>?![\x5c\]{}]+))(?:\s*+ \s*+(?:(?:charset\s*+=\s*+(?:\"?(?:iso-8859-15?|windows-1252|utf-8)\b\"?))|(?:(?:c(?:h(?:a(?:r(?:s(?:e[^t\"(),\/: <=> (714 characters omitted)' against variable `TX:1' (Value: `text/plain charset=utf-8\x0d\x0a' ) [file "/etc/nginx/owasp-modsecurity-crs/rules/REQUEST-922-MULTIPART-ATTACK.conf"] [line "51"] [id "922110"] [rev ""] [msg "Illegal MIME Multipart Header content-type: charset parameter"] [data "Matched Data: text/plain charset=utf-8\x0d\x0a found within Content-Type multipart form"] [severity "2"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/272/220"] [tag "paranoia-level/1"] [hostname "10.244.3.23"] [uri "/REDACTED"] [unique_id "167446597097.337049"] [ref "o0,41o14,27v2197,41t:lowercaset:lowercase"]

My observation is that my and the original request that @chladic posted both contain multi-part form data in the body, and the false positive trigger seems to be something to do with the fact that a Content-Type is defined in one of the parts.

The warning seems to suggest that the parsing of that additional content type header is not excluding the trailing CR/LF from the string it's trying to match (e.g. text/plain charset=utf-8 becomes text/plain charset=utf-8\x0d\x0a) and is as such being rejected.

Edit: Actually, just a thought: At what point are the CR/LF being escaped to \x0d\x0a? Is it just for presentation in the logs, or is something escaping them before the regexes in the rule are being run?

@theseion
Copy link
Contributor

I see. Let's keep it here then. You might be right that crlf is being escaped for the logs, but those bytes shouldn't even be part of the header value.

We haven't been able to recreate the issue so far, so it looks like it has to be a specific request format. Given that you are able to capture the request that triggers the issue, could you try and capture the actual bytes in the request (with tcpdump for example) and then do the same for a request that is as similar as possible but doesn't trigger? That would at least tell us which byte sequence to use to trigger the this.

@mikegoatly
Copy link

mikegoatly commented Jan 25, 2023

Ok, this request fails whenever I throw it against an API fronted by ModSecurity:

POST https://YOURURLGOESHERE/
Content-Type: multipart/form-data; boundary="----x"

------x
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=title

Test
------x
Content-Disposition: form-data; name=file; filename="New Text Document.txt"; filename*=utf-8''New%20Text%20Document.txt

Some sample text

------x--

(It's in the .http file format, so you can send it from something like VS Code with the REST Client extension.)

I've tried the request with both CRLF and LF line endings, both fail.

Stripping the ; charset=utf-8 from the header in the form data makes it work:

POST https://YOURURLGOESHERE/
Content-Type: multipart/form-data; boundary="----x"

------x
Content-Type: text/plain;
Content-Disposition: form-data; name=title

Test
------x
Content-Disposition: form-data; name=file; filename="New Text Document.txt"; filename*=utf-8''New%20Text%20Document.txt

Some sample text

------x--

I've attached the failing request so you can see the exact content that's being sent from my machine: request.zip

Does that help?

@theseion
Copy link
Contributor

That's awesome! Thanks a lot @mikegoatly. Well try to reproduce the issue on our end.

@airween
Copy link
Contributor

airween commented Jan 25, 2023

That's awesome! Thanks a lot @mikegoatly. Well try to reproduce the issue on our end.

Let me check that it soon.

@airween
Copy link
Contributor

airween commented Jan 25, 2023

@mikegoatly, I tried to reproduce your issue, also with your suggested way (REST client for VSCode - thanks for the tip), but no luck.

Ok, this request fails whenever I throw it against an API fronted by ModSecurity:

now I made a small Python script which sends the raw format of your request, you can see that here. First, please check that, and let me know if there is something not well.

Then could you try it against your application? (I see you use HTTPS, but actually in my dev env I do not have any HTTPS, and it's more convenient to try the raw requests like this without HTTPS).

This is what I see when I run the script:

SEND REQUEST:
=============

POST /dump.php HTTP/1.1
Host: localhost
Accept: */*
User-Agent: PyTcpClient v0.1
Content-Type: multipart/form-data; boundary="----x"
Content-Length: 253

------x
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=title

Test
------x
Content-Disposition: form-data; name=file; filename="New Text Document.txt"; filename*=utf-8''New%20Text%20Document.txt

Some sample text

------x--

====

RECEIVED RESPONSE:
==================

HTTP/1.1 400 Bad Request
Date: Wed, 25 Jan 2023 21:24:43 GMT
Server: Apache/2.4.54 (Debian)
Content-Length: 301
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.54 (Debian) Server at localhost Port 80</address>
</body></html>

and this is what I see in my log:

[client 127.0.0.1:34804] [client 127.0.0.1] ModSecurity: Multipart parsing error: Multipart: Invalid Content-Disposition header (-11): form-data; name=file; filename="New Text Document.txt"; filename*=utf-8''New%20Text%20Document.txt. [hostname "localhost"] [uri "/dump.php"] [unique_id "Y9GeG__D42q-U0HlyXxqpAAAAAA"]
[client 127.0.0.1:34804] [client 127.0.0.1] ModSecurity: Access denied with code 400 (phase 2). Match of "eq 0" against "REQBODY_ERROR" required. [file "/etc/modsecurity/modsecurity.conf"] [line "75"] [id "200002"] [msg "Failed to parse request body."] [data "Multipart parsing error: Multipart: Invalid Content-Disposition header (-11): form-data; name=file; filename=\\x22New Text Document.txt\\x22; filename*=utf-8''New%20Text%20Document.txt."] [severity "CRITICAL"] [hostname "localhost"] [uri "/dump.php"] [unique_id "Y9GeG__D42q-U0HlyXxqpAAAAAA"]

or with Nginx+libmodsecurity3:

[client 127.0.0.1] ModSecurity: Access denied with code 400 (phase 2). Matched "Operator `Eq' with parameter `0' against variable `MULTIPART_STRICT_ERROR' (Value: `1' ) [file "/etc/nginx/modsecurity.conf"] [line "65"] [id "200003"] [rev ""] [msg "Multipart request body failed strict validation: \\x0aPE 0, \\x0aBQ 1, \\x0aBW 0, \\x0aDB 0, \\x0aDA 0, \\x0aHF 0, \\x0aLF 1, \\x0aSM 0, \\x0aIQ 0, \\x0aIP 0, \\x0aIH 0, \\x0aFL "]

So as you can see there is only one rule triggered in phase 2 with id 200002 (with both implementations):

Multipart parsing error: Multipart: Invalid Content-Disposition header (-11): form-data; name=file; filename="New Text Document.txt"; filename*=utf-8''New%20Text%20Document.txt.

and

Access denied with code 400 (phase 2). Match of "eq 0" against "REQBODY_ERROR" required.

I'm using CRS with Apache 2.9.7, and Nginx 1.18 and libmodsecurity3 3.0.8.

Could you check that script with your application, and align it to reproduce the issue?

@mikegoatly
Copy link

Thanks @airween - I was unable to reproduce the problem using variations of your script, so I figured it must be something to do with the way that the request is being formed. I had a play with a different way of building the request using Python's requests module and I can now reproduce the problem from a Linux environment as well (I'm normally running my requests from a Windows environment, so I wanted to rule that out).

The script I ended up with is:

import requests

files = {'file': ("New Text Document.txt", "Test", "text/plain; charset=utf-8")}

response = requests.post("http://testapi/test", files=files)

print("REQUEST:")
print(response.request.body[:200])

print("")
print("RESPONSE:")
print("STATUS: %s %s" % (response.status_code, response.reason))
print("CONTENT:")
print(response.text)

Running this for me gives the output:

REQUEST:
b'--09c98bf95bed186d31baf8cd9284cd14\r\nContent-Disposition: form-data; name="file"; filename="New Text Document.txt"\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nTest\r\n--09c98bf95bed186d31baf8cd9284cd14--'

RESPONSE:
STATUS: 403 Forbidden
CONTENT:
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

The logs show the exact errors and warnings that have been reported here.

And if I remove charset=utf-8 from the content type in the files variable, then the request goes through.

What happens in your environment if you make a request using that script?

@airween
Copy link
Contributor

airween commented Jan 26, 2023

@mikegoatly - thanks for script.

One of the reason why I didn't want to use Python's request module that it triggers the rule 913101:

ModSecurity: Warning. Matched phrase "python-requests" at REQUEST_HEADERS:User-Agent. [file "/home/airween/src/coreruleset/rules/REQUEST-913-SCANNER-DETECTION.conf"] [line "143"] [id "913101"] [msg "Found User-Agent associated with scripting/generic HTTP client"] [data "Matched Data: python-requests found within REQUEST_HEADERS:User-Agent: python-requests/2.25.1"] [severity "CRITICAL"] [ver "OWASP_CRS/3.3.4"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-reputation-scripting"] [tag "OWASP_CRS"] [tag "capec/1000/118/224/541/310"] [tag "PCI/6.5.10"] [tag "paranoia-level/2"] [hostname "localhost"] [uri "/dump.php"] [unique_id "Y9KFbLAP0-kJr5b5OqfNjAAAAAE"]

The other one is that request formats the HTTP request as well, therefore I can't send any invalid request :).

So I changed the User-Agent header in request:

... files=files, headers={'User-Agent': 'PyTest Client v0.1'})

and run the script.

No rule was triggered. None of them.

REQUEST:
b'--7e824900b7127ebbb2dd7b2982b35061\r\nContent-Disposition: form-data; name="file"; filename="New Text Document.txt"\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nTest\r\n--7e824900b7127ebbb2dd7b2982b35061--'

RESPONSE:
STATUS: 200 OK
CONTENT:
...

Note: in my dev env I have CRS v4.0/dev. Which version of CRS do you use?

@mikegoatly
Copy link

OK, silly question time - I'm running ModSecurity as part of ingress-nginx. What's the best way to find that version number for you?

@theseion
Copy link
Contributor

ingress-nginx should be a container image, right? If you can get hold of the image, or the definition, you should be able to find out.

@mikegoatly
Copy link

Thanks @theseion - @airween Looks like it's running:

CRS v3.3.4
ModSecurity v3.0.8

For what it's worth, I've just tried adding the same user agent header as you, and I still get the 403 response.

@airween
Copy link
Contributor

airween commented Jan 26, 2023

@mikegoatly - thanks.

I was able to reproduce the issue on Nginx + libmodsecurty3 3.0.8 and CRS 3.3.4.

2023/01/26 22:15:12 [info] 121615#121615: *1 ModSecurity: Warning. Matched "Operator `Rx' with parameter `^(?:(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+)\/(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+))(?:\s*+;\s*+(?:(?:charset\s*+=\s*+(?:\"?(?:iso-8859-15?|windows-1252|utf-8)\b\"?))|(?:(?:c(?:h(?:a(?:r(?:s(?:e[^t\"(),\/:;<=> (714 characters omitted)' against variable `TX:1' (Value: `text/plain; charset=utf-8\x0d\x0a' ) [file "/home/airween/src/coreruleset/rules/REQUEST-922-MULTIPART-ATTACK.conf"] [line "51"] [id "922110"] [rev ""] [msg "Illegal MIME Multipart Header content-type: charset parameter"] [data "Matched Data: text/plain; charset=utf-8\x0d\x0a found within Content-Type multipart form"] [severity "2"] [ver "OWASP_CRS/3.3.4"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-protocol"] [tag "OWASP_CRS"] [tag "capec/272/220"] [tag "paranoia-level/1"] [hostname "::1"] [uri "/dump.php"] [unique_id "167476771293.913423"] [ref "o0,41o14,27v356,41t:lowercaset:lowercase"], client: ::1, server: _, request: "POST /dump.php HTTP/1.1", host: "localhost:8080"

Let me investigate the reasons, why happens this (and why does not with Apache).

@airween
Copy link
Contributor

airween commented Jan 27, 2023

Thank you for your patience.

Looks like libmodsecurity3 handles buggy the multipart content types (too?), because it keeps the trailing white space chars, namely \r and \n in our case.

What can you do now?

I've found a workaround, this patch worked for me:

diff --git a/rules/REQUEST-922-MULTIPART-ATTACK.conf b/rules/REQUEST-922-MULTIPART-ATTACK.conf
index 1bf5a03..7c2fd36 100644
--- a/rules/REQUEST-922-MULTIPART-ATTACK.conf
+++ b/rules/REQUEST-922-MULTIPART-ATTACK.conf
@@ -53,7 +53,7 @@ SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*+:\s*+(.*)$" \
     phase:2,\
     block,\
     capture,\
-    t:none,t:lowercase,\
+    t:none,t:lowercase,t:removeWhiteSpace,\
     msg:'Illegal MIME Multipart Header content-type: charset parameter',\
     logdata:'Matched Data: %{TX.1} found within Content-Type multipart form',\
     tag:'application-multi',\
@@ -67,7 +67,7 @@ SecRule MULTIPART_PART_HEADERS "@rx ^content-type\s*+:\s*+(.*)$" \
     severity:'CRITICAL',\
     chain"
     SecRule TX:1 "!@rx ^(?:(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+)\/(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+))(?:\s*+;\s*+(?:(?:charset\s*+=\s*+(?:\"?(?:iso-8859-15?|windows-1252|utf-8)\b\"?))|(?:(?:c(?:h(?:a(?:r(?:s(?:e[^t\"(),\/:;<=>?![\x5c\]{}]|[^e\"(),/:;<=>?![\x5c\]{}])|[^s\"(),/:;<=>?![\x5c\]{}])|[^r\"(),/:;<=>?![\x5c\]{}])|[^a\"(),/:;<=>?![\x5c\]{}])|[^h\"(),/:;<=>?![\x5c\]{}])|[^c\"(),/:;<=>?![\x5c\]{}])[^\"(),/:;<=>?![\x5c\]{}]*(?:)\s*+=\s*+[^(),/:;<=>?![\x5c\]{}]+)|;?))*(?:\s*+,\s*+(?:(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+)\/(?:\*|[^\"(),\/:;<=>?![\x5c\]{}]+))(?:\s*+;\s*+(?:(?:charset\s*+=\s*+(?:\"?(?:iso-8859-15?|windows-1252|utf-8)\b\"?))|(?:(?:c(?:h(?:a(?:r(?:s(?:e[^t\"(),\/:;<=>?![\x5c\]{}]|[^e\"(),/:;<=>?![\x5c\]{}])|[^s\"(),/:;<=>?![\x5c\]{}])|[^r\"(),/:;<=>?![\x5c\]{}])|[^a\"(),/:;<=>?![\x5c\]{}])|[^h\"(),/:;<=>?![\x5c\]{}])|[^c\"(),/:;<=>?![\x5c\]{}])[^\"(),/:;<=>?![\x5c\]{}]*(?:)\s*+=\s*+[^(),/:;<=>?![\x5c\]{}]+)|;?))*)*$" \
-        "t:lowercase,\
+        "t:lowercase,t:removeWhiteSpace,\
         setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'"
 
 # Content-Transfer-Encoding was deprecated by rfc7578 in 2015 and should not be used (see: https://www.rfc-editor.org/rfc/rfc7578#section-4.7)

so added the t:removeWhiteSpace transformation solved the problem, and rule 922110 didn't trigger.

(ping @fzipi as author of the rule: what do you think? Does it make sense to add this permanently?)

If you want to avoid the unwanted other REQUEST_BODY matches, you can add something similar:

SecRule REQUEST_HEADERS:Content-Type "@beginsWith multipart/form-data" \
    "id:1000001,\
    phase:1,\
    pass,\
    t:none,t:lowercase,\
    nolog,\
    ctl:ruleRemoveTargetByTag=OWASP_CRS;REQUEST_BODY"

@theseion
Copy link
Contributor

I guess we could use removeWhiteSpace. It would make the regular expression simpler as well. I'll open an issue for that. Are you going to open an issue with ModSecurity? The line break characters should definitely not be part of the header value.

@chladic
Copy link
Author

chladic commented Jan 27, 2023

Thank you everyone for finding solution to this. Really appreciate

@airween
Copy link
Contributor

airween commented Jan 27, 2023

Are you going to open an issue with ModSecurity?

I try to find the bug and send a PR to fix it. If do not have time, I just open an issue.

@airween
Copy link
Contributor

airween commented Apr 24, 2023

Just FYI: the sent patch above for libmodsecurity3 has been merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Something isn't working ⏳ awaiting feedback CRS dev asked feedback ⚠️ ModSec Issue related to ModSecurity
Projects
None yet
Development

No branches or pull requests

6 participants