Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions nginx/conf.d/spx-bot-mitigation-logic.conf
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,12 @@ map "$ua_block_type:$empty_ua_geo_block" $block_reason {
"1:1" "malformed_or_scanner";
default "";
}

# 6. Hard-block subset used by upload handlers.
# Upload routes may bypass soft UA/geo-only signals to reduce false positives,
# but must still enforce hard security blocks (scanner/header-injection/legacy UA).
map $block_reason $hard_block_reason {
default $block_reason;
"empty_user_agent" "";
"empty_ua_high_risk_geo" "";
}
Comment thread
Copilot marked this conversation as resolved.
19 changes: 12 additions & 7 deletions nginx/snippets/spx-upload-limits.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
# Shared WordPress upload route overrides
# Keep this snippet included in every HTTPS WordPress vhost server block.
# -----------------------------------------------------------------------------
#
# NOTE: These upload handlers bypass soft $block_reason UA/geo checks to avoid
# false-positive 403s on legitimate upload clients, but still enforce
# $hard_block_reason hard security blocks. Method restrictions and rate limits
# still apply in these handlers, while upload authorization is enforced by
# WordPress upstream.

# WordPress plugin/theme ZIP uploader — raised body limit for this single
# handler only. update.php processes plugin and theme uploads submitted via
Expand All @@ -10,8 +16,6 @@
# than on the broad /wp-admin/ prefix so all other admin routes stay at the
# safe 10m global default.
location = /wp-admin/update.php {
if ($block_reason != "") { return 403; }

# 64m covers the vast majority of plugin/theme archives while remaining
# well below the 100m cap used for media uploads.
client_max_body_size 64m;
Expand All @@ -25,6 +29,7 @@ location = /wp-admin/update.php {
deny all;
}

if ($hard_block_reason != "") { return 403; }
limit_req zone=spx_wp_admin burst=150 nodelay;
include /etc/nginx/snippets/spx-standard-proxy-headers.conf;
include /etc/nginx/snippets/spx-dynamic-proxy-headers.conf;
Expand All @@ -33,8 +38,6 @@ location = /wp-admin/update.php {

# WP admin async upload — POST only; uses wp_admin rate zone.
location = /wp-admin/async-upload.php {
if ($block_reason != "") { return 403; }

client_max_body_size 100m;

if ($request_method = OPTIONS) {
Expand All @@ -46,16 +49,17 @@ location = /wp-admin/async-upload.php {
deny all;
}

if ($hard_block_reason != "") { return 403; }
limit_req zone=spx_wp_admin burst=150 nodelay;
include /etc/nginx/snippets/spx-standard-proxy-headers.conf;
include /etc/nginx/snippets/spx-dynamic-proxy-headers.conf;
proxy_pass http://varnish_backend;
}

# WP REST API media — GET/HEAD for reads, POST for uploads, OPTIONS for CORS.
location ~* ^/wp-json/wp/v2/media {
if ($block_reason != "") { return 403; }

# Pattern matches only the collection endpoint and numeric single-item paths
# (/wp-json/wp/v2/media and /wp-json/wp/v2/media/{id}); nothing broader.
location ~* ^/wp-json/wp/v2/media(/[0-9]+)?$ {
client_max_body_size 100m;
Comment thread
MaximillianGroup marked this conversation as resolved.

if ($request_method = OPTIONS) {
Expand All @@ -67,6 +71,7 @@ location ~* ^/wp-json/wp/v2/media {
deny all;
}

if ($hard_block_reason != "") { return 403; }
limit_req zone=spx_general burst=200 nodelay;
include /etc/nginx/snippets/spx-standard-proxy-headers.conf;
include /etc/nginx/snippets/spx-dynamic-proxy-headers.conf;
Expand Down
33 changes: 33 additions & 0 deletions var/www/html/.user.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
; =============================================================================
; PHP-FPM per-directory upload size overrides
; Deployed to: /var/www/html/.user.ini
;
; PHP-FPM scans for .user.ini files in the directory containing each requested
; PHP file and all parent directories up to the document root, and caches the
; result for user_ini.cache_ttl seconds (default typically 300). Settings placed
; here apply to every PHP request served from /var/www/html/.
;
Comment thread
Copilot marked this conversation as resolved.
; NOTE: upload_max_filesize and post_max_size are PHP_INI_PERDIR entries that
; must be configured via php.ini or an FPM pool .conf (not via ini_set()).
; Web access to this file is blocked by the Nginx dotfile deny (location ~ /\. { deny all; })
; and by Apache common-settings.conf (<Files "\.(env|ini|mo|conf|log|bak|htaccess|example|git|svn)$"> Require all denied).
;
; These values are aligned with the nginx per-route body-size limits:
; /wp-admin/update.php → client_max_body_size 64m
; /wp-admin/async-upload.php → client_max_body_size 100m
; /wp-json/wp/v2/media → client_max_body_size 100m
;
; upload_max_filesize: individual file cap — set below the 100m Nginx limit so a
; multipart/form-data request (file + boundaries + fields) stays under the
; upstream request-size cap.
;
; post_max_size: total POST body cap — must exceed upload_max_filesize to leave
; room for multipart boundary and form fields. 100M provides ~5M headroom above
; the 95M file cap.
;
; Cloudflare hard cap: ~100 MB per request (plan-dependent). The effective limit
; is still capped upstream.
; =============================================================================

upload_max_filesize = 95M
post_max_size = 100M
Comment on lines +32 to +33
Comment on lines +32 to +33
Loading