Skip to content

File upload finishes with MULTIPART_UNMATCHED_BOUNDARY error #244

Closed
@airween

Description

@airween

I always run into a MULTIPART_UNMATCHED_BOUNDARY error when I upload any kind of file.

The test environment is @defanator's Vagrant image.

Components:

test@vagrant:~/ModSecurity$ git describe 
v3.0.4-118-g4127c1bf

test@vagrant:~/ModSecurity-nginx$ git describe 
v1.0.1-23-g21bc821

/usr/sbin/nginx -V
nginx version: nginx/1.19.6
built by gcc 8.3.0 (Debian 8.3.0-6) 
built with OpenSSL 1.1.1d  10 Sep 2019
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules \
 --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log \
 --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock \
 --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
 --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
 --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio \
 --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module \
 --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module \
 --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module \
 --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail \
 --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module \
 --with-stream_ssl_preread_module --add-dynamic-module=../ModSecurity-nginx

This is what I modified:

diff --git a/states/files/etc/nginx/fastcgi_params b/states/files/etc/nginx/fastcgi_params
new file mode 100644
index 0000000..28decb9
--- /dev/null
+++ b/states/files/etc/nginx/fastcgi_params
@@ -0,0 +1,25 @@
+
+fastcgi_param  QUERY_STRING       $query_string;
+fastcgi_param  REQUEST_METHOD     $request_method;
+fastcgi_param  CONTENT_TYPE       $content_type;
+fastcgi_param  CONTENT_LENGTH     $content_length;
+
+fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
+fastcgi_param  REQUEST_URI        $request_uri;
+fastcgi_param  DOCUMENT_URI       $document_uri;
+fastcgi_param  DOCUMENT_ROOT      $document_root;
+fastcgi_param  SERVER_PROTOCOL    $server_protocol;
+fastcgi_param  REQUEST_SCHEME     $scheme;
+fastcgi_param  HTTPS              $https if_not_empty;
+
+fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
+fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
+
+fastcgi_param  REMOTE_ADDR        $remote_addr;
+fastcgi_param  REMOTE_PORT        $remote_port;
+fastcgi_param  SERVER_ADDR        $server_addr;
+fastcgi_param  SERVER_PORT        $server_port;
+fastcgi_param  SERVER_NAME        $server_name;
+
+# PHP only, required if PHP was built with --enable-force-cgi-redirect
+fastcgi_param  REDIRECT_STATUS    200;
diff --git a/states/files/etc/nginx/modsec/main.conf b/states/files/etc/nginx/modsec/main.conf
index 16af6e5..fc181a0 100644
--- a/states/files/etc/nginx/modsec/main.conf
+++ b/states/files/etc/nginx/modsec/main.conf
@@ -1,5 +1,5 @@
 include /etc/nginx/modsec/modsecurity.conf
 
 # OWASP CRS rules
-include /etc/nginx/modsec/owasp-crs/crs-setup.conf
-include /etc/nginx/modsec/owasp-crs/rules/*.conf
+#include /etc/nginx/modsec/owasp-crs/crs-setup.conf
+#include /etc/nginx/modsec/owasp-crs/rules/*.conf
diff --git a/states/files/etc/nginx/modsec/modsecurity.conf b/states/files/etc/nginx/modsec/modsecurity.conf
new file mode 100644
index 0000000..284e543
--- /dev/null
+++ b/states/files/etc/nginx/modsec/modsecurity.conf
@@ -0,0 +1,49 @@
+SecRuleEngine On
+SecRequestBodyAccess On
+SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \
+     "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
+SecRule REQUEST_HEADERS:Content-Type "application/json" \
+     "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON,ctl:ruleRemoveById=930120"
+SecRequestBodyLimit 13107200
+SecRequestBodyNoFilesLimit 131072
+SecRequestBodyLimitAction Reject
+SecRule REQBODY_ERROR "!@eq 0" \
+"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
+SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
+"id:'200003',phase:2,t:none,log,deny,status:400, \
+msg:'Multipart request body failed strict validation: \
+PE %{REQBODY_PROCESSOR_ERROR}, \
+BQ %{MULTIPART_BOUNDARY_QUOTED}, \
+BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
+DB %{MULTIPART_DATA_BEFORE}, \
+DA %{MULTIPART_DATA_AFTER}, \
+HF %{MULTIPART_HEADER_FOLDING}, \
+LF %{MULTIPART_LF_LINE}, \
+SM %{MULTIPART_MISSING_SEMICOLON}, \
+IQ %{MULTIPART_INVALID_QUOTING}, \
+IP %{MULTIPART_INVALID_PART}, \
+IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
+FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
+SecRule MULTIPART_UNMATCHED_BOUNDARY "@gt 1" \
+    "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
+SecPcreMatchLimit 1000
+SecPcreMatchLimitRecursion 1000
+SecRule TX:/^MSC_/ "!@streq 0" \
+        "id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'"
+SecResponseBodyAccess On
+SecResponseBodyMimeType text/plain text/html text/xml
+SecResponseBodyLimit 524288
+SecResponseBodyLimitAction ProcessPartial
+SecTmpDir /tmp/
+SecDataDir /tmp/
+SecDebugLog /var/log/nginx/modsec_debug.log
+SecDebugLogLevel 9
+SecAuditEngine On
+SecAuditLogRelevantStatus "^(?:5|4(?!04))"
+SecAuditLogParts ABCDEFHIJZ
+SecAuditLogType Serial
+SecAuditLog /var/log/modsec_audit.log
+SecArgumentSeparator &
+SecCookieFormat 0
+SecUnicodeMapFile unicode.mapping 20127
+SecStatusEngine On
diff --git a/states/files/etc/nginx/nginx.conf b/states/files/etc/nginx/nginx.conf
index 15cd224..0cb2772 100644
--- a/states/files/etc/nginx/nginx.conf
+++ b/states/files/etc/nginx/nginx.conf
@@ -33,6 +33,10 @@ http {
         server 127.0.0.1:3131;
     }
 
+    upstream php-fpm {
+        server unix:/run/php/php7.3-fpm.sock;
+    }
+
     server {
         listen 8080 default_server backlog=32000;
         location / {
@@ -67,6 +71,8 @@ http {
             include uwsgi_params;
             uwsgi_pass upload-app;
             client_max_body_size 256m;
+            modsecurity on;
+            modsecurity_rules_file /etc/nginx/modsec/main.conf;
         }
 
         location /modsec-light/ {
@@ -94,6 +100,15 @@ http {
             include uwsgi_params;
             uwsgi_pass upload-app;
         }
+        location ~ \.php$ {
+            modsecurity on;
+            modsecurity_rules_file /etc/nginx/modsec/main.conf;
+            fastcgi_pass  php-fpm;
+            fastcgi_index index.php;
+            fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name;
+            fastcgi_intercept_errors on;
+            include fastcgi_params;
+        }
     }
 
     include /etc/nginx/conf.d/*.conf;

Short summary of modifications:

  • added fastcgi_params to nginx
  • turned off CRS
  • added default modsecurity.conf - few things were modifed
  • set up PHP FPM

Here is the PHP file what I used:

# cat /var/www/html/index.php 
<?php

var_dump($_POST);

The file what I try to upload:

$ cat text.txt 
test text

curl commands:

$ curl -vvv "http://localhost/upload/" -F "data=@text.txt"
* Expire in 0 ms for 6 (transfer 0x56389b68df50)
...
* Expire in 1 ms for 1 (transfer 0x56389b68df50)
*   Trying ::1...
* TCP_NODELAY set
* Expire in 149998 ms for 3 (transfer 0x56389b68df50)
* Expire in 200 ms for 4 (transfer 0x56389b68df50)
* connect to ::1 port 80 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 149998 ms for 3 (transfer 0x56389b68df50)
* Connected to localhost (127.0.0.1) port 80 (#0)
> POST /upload/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Length: 196
> Content-Type: multipart/form-data; boundary=------------------------0e87336e7e623e42
> 
< HTTP/1.1 403 Forbidden
< Server: nginx/1.19.6
< Date: Thu, 01 Apr 2021 19:06:25 GMT
< Content-Type: text/html
< Content-Length: 153
< Connection: keep-alive
* HTTP error before end of send, stop sending
< 
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.6</center>
</body>
</html>
* Closing connection 0

in the error.log:

2021/04/01 19:06:25 [error] 3329#3329: *2 [client 127.0.0.1] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Gt' with parameter `1' against variable `MULTIPART_UNMATCHED_BOUNDARY' (Value: `2' ) [file "/etc/nginx/modsec/modsecurity.conf"] [line "15"] [id "200004"] [rev ""] [msg "Multipart parser detected a possible unmatched boundary."] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/upload/"] [unique_id "1617303985"] [ref "v180,1"], client: 127.0.0.1, server: , request: "POST /upload/ HTTP/1.1", host: "localhost"

Note: this request has sent to /upload/ which was already configured.

Another curl command:

$ curl -vvv "http://localhost/index.php" -F "data=@text.txt"
* Expire in 0 ms for 6 (transfer 0x558bc0435f50)
...
* Expire in 0 ms for 1 (transfer 0x558bc0435f50)
*   Trying ::1...
* TCP_NODELAY set
* Expire in 149999 ms for 3 (transfer 0x558bc0435f50)
* Expire in 200 ms for 4 (transfer 0x558bc0435f50)
* connect to ::1 port 80 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 149998 ms for 3 (transfer 0x558bc0435f50)
* Connected to localhost (127.0.0.1) port 80 (#0)
> POST /index.php HTTP/1.1
> Host: localhost
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Length: 196
> Content-Type: multipart/form-data; boundary=------------------------9dd881243a0bfb97
> 
< HTTP/1.1 403 Forbidden
< Server: nginx/1.19.6
< Date: Thu, 01 Apr 2021 19:08:37 GMT
< Content-Type: text/html
< Content-Length: 153
< Connection: keep-alive
* HTTP error before end of send, stop sending
< 
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.19.6</center>
</body>
</html>
* Closing connection 0

in the error log I got:

2021/04/01 19:08:37 [error] 3329#3329: *3 [client 127.0.0.1] ModSecurity: Access denied with code 403 (phase 2). Matched "Operator `Gt' with parameter `1' against variable `MULTIPART_UNMATCHED_BOUNDARY' (Value: `2' ) [file "/etc/nginx/modsec/modsecurity.conf"] [line "15"] [id "200004"] [rev ""] [msg "Multipart parser detected a possible unmatched boundary."] [data ""] [severity "0"] [ver ""] [maturity "0"] [accuracy "0"] [hostname "127.0.0.1"] [uri "/index.php"] [unique_id "1617304117"] [ref "v182,1"], client: 127.0.0.1, server: , request: "POST /index.php HTTP/1.1", host: "localhost"

When I run tcpdump, I see that there IS the final boundary in the request. I also tried it from browser, final boundary also at there.

Am I spoiling something? Or is this a really unexpected behavior?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions