Skip to content

Instantly share code, notes, and snippets.

@JeroenBoersma
Last active April 9, 2026 11:18
Show Gist options
  • Select an option

  • Save JeroenBoersma/d5c33066d7b0a7c69736a3d0f67ac9b1 to your computer and use it in GitHub Desktop.

Select an option

Save JeroenBoersma/d5c33066d7b0a7c69736a3d0f67ac9b1 to your computer and use it in GitHub Desktop.
NGINX - Polyshell blocker

Currently Polyshell is mass hitting many servers.

Current solutions

  • Install Sansec Shield to protect your shop this and future attacks
  • Install the backport of the Magento 2.4.9 patch build by Mark Shust
  • Apply the NGINX contents below to your configuration to not be affected by conflicting location ... {} rules

NGINX patch

To not hit Magento at all, apply the files below to your NGINX configuration.

Currently some sites have rules to forward *.php to the PHP backend, uploading a file to /media/custom_options/a/b/index.php can still be forwarded to the backend despite having the rule:

location *.php {
  # send to php
  fastcgi_...;
}

location /media/custom_options {
  deny all;
  return 403;
}

The first location rule will send the file to the backend.

By applying both patches below, you are not affected by the location rules and will return proper 403's.

Installation

Configuration

If you rely on services that still need to access the rest endpoint, add them to the $polyshell_blocked_addr map. Allowing is simple as

map $remote_addr $polyshell_blocked_addr {
  127.0.0.1 0; # allow localhost
  10.10.0.0/16 0; # allow the 10.10.*.* subnet
  default 1;
}

If urls should change, you can use the $polyshell_blocked_method_uri map. It combines METHOD (POST|GET|DELETE|PATCH|PUT) /path/to/server for matching.

Hosting specific

Hypernode

On Hypernode you can apply this patch as easy as one two three.

Blocking is done before it reaches the backend.

  • copy http.polyshell_blocked.conf to ~/nginx/http.polyshell_blocked.conf
  • list your active domains hypernode-manage-vhosts --list --format json | jq -r '. | keys | .[]'
  • Copy server.polyshell_blocked.conf to each ~/nginx/{DOMAIN}/pub.polyshell_blocked.conf
  • Nginx will reload automatically

Maxcluster

From the web interface.

  • Login and navigate to your server
  • Edit the NGINX.CONF (button) configuration, add contents of http.polyshell_blocked.conf to the nginx.conf in the http{...} section.
  • Select your site from list, edit the rules, add contents of server.polyshell_blocked.conf to the /etc/nginx/sites-available/maxcluster.hyva.io/userdefined.conf.init section.

Research

Thanks to Sansec, publishing their excelent research https://sansec.io/research/magento-polyshell

Live

Sansec tracks active attacks around the Globe.

https://sansec.io/live

If you were hit or want to know if you were hit (changes are high) with the Polyshell file uploads.

Automated scan

Run Sansec ecomscan to see if there are files that shouldn't be there.

Manual scanning Polyshell files.

If you are not using the file feature for orders, there should only be a .htaccess

Searching for other files, could take a while if you do not know where exactly to search.

# From your Magento root dir
find pub/media/custom_options -type f -not -name '.htaccess'

# If you don't know where your root dir is
# Search for all directories called custom_options and show the files in there
find . -maxdepth 6 -not -type l -type d -name custom_options | grep /media/custom_options | xargs realpath | sort | uniq | while read a; do find "$a" -type f -not -name '.htaccess'; done

Removing those files

You should remove the files and check if they were not succesfully executed, see next chapter.

Research your logs

Take a look at your logs to see if files are being accessed.

# This will show the POST|PUT requests to the API
grep -E '/V1/guest-carts/.+/items' /var/log/nginx/access.log

# This will show the requests done to the custom_options path
grep -E '/media/custom_options/' /var/log/nginx/access.log

200 OK's is not what you want to see.

Statistics

Presume you have jq installed.

# Finding attackers IP's
# regular logs, ip position 1
grep -E '(/media/custom_options/|/V1/guest-carts/[^/]+/items)' /var/log/nginx/access.log | awk '{ print $1; }' | jq -s 'group_by(.) | map(.[0] + " " + (. | length | tostring)) | .[]' -r | sort -n | uniq

# JSON logs, .remote_addr contains the ip
grep -E '(/media/custom_options/|/V1/guest-carts/[^/]+/items)' /var/log/nginx/access.log | jq .remote_addr | jq -s 'group_by(.) | map(.[0] + " " + (. | length | tostring)) | .[]' -r | sort -n | uniq

Outputs

1.2.3.4 1
1.2.3.5 1112
...
# 20260331 JB: Polyshell nginx level blocking
# Allow trusted IP's or IP blocks to the endpoint if needed
map $remote_addr $polyshell_blocked_addr {
# Allowlist addresses
# 1.2.3.4 0;
# 1.2.3.4/24 0;
default 1; # block by default
}
# Check for uris, /V1/guest-carts/[cartId]/items is vulnerable (don't bother where the API is stored)
map "${request_method} ${request_uri}" $polyshell_blocked_method_uri {
"~(POST|PUT) .*/V1/guest-carts/[^/]+/items" 1; # POST and PUT on the API
"~(\S*) .*/media/custom_options/" 1; # ANY on the media directory
default 0;
}
# Combinations will be blocked
map "${polyshell_blocked_addr}${polyshell_blocked_method_uri}" $polyshell_blocked {
"11" 1;
default 0;
}
# ...
# server {
# ...
# 20260331 JB: Polyshell nginx level blocker
# Rules are in the http.polyshell_blocked.conf file
# rules exists of IP allow listing and blocking,
# and combined with request method and path
# Return 403 even if location rules exist
if ($polyshell_blocked = 1) {
return 403;
}
# ...
# }
# ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment