Skip to content

Commit f4c0f42

Browse files
committed
[security] Bound URL-based image transforms to prevent memory-exhaustion DoS
URL query image actions (e.g. `?resize=`) are now disabled by default via `system.images.url_actions`, and when enabled are rejected above a configurable total-pixel ceiling (`system.images.max_pixels`, default 25M). This closes the unauthenticated DoS where oversized resize dimensions drove unbounded GD/Imagick memory allocation outside PHP's memory_limit (CWE-400, GHSA-4x9g-vw65-vvf9). Backport of the 2.0 fix to the 1.7 line. Reported by @iliaal.
1 parent 8d5082b commit f4c0f42

4 files changed

Lines changed: 54 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# v1.7.53
2-
## 06/09/2026
2+
## 06/10/2026
33

44
1. [](#bugfix)
55
* [security] Direct web access to the `user/accounts`, `user/config`, `user/data` and `user/env` folders is now blocked outright in every bundled webserver config, closing a hole where files such as certificates, tokens and databases stored under `user/data` with an unlisted extension could be downloaded directly.
66
* [security] A backup deny-all `.htaccess` now ships inside `user/accounts`, `user/config` and `user/data` so Apache installs stay protected even when the site root `.htaccess` has been customised or is out of date.
77
* [security] The upgrade postflight now patches an existing stock root `.htaccess` to add the folder block automatically, so installs that updated from an earlier version are protected without editing the file by hand.
8+
* [security] URL query image transforms (such as `image.jpg?resize=`) are now turned off by default and, when enabled, refuse oversized dimensions above a configurable pixel limit, closing an unauthenticated denial of service where huge resize values could exhaust server memory.
89

910
# v1.7.52
1011
## 04/29/2026

‎system/blueprints/config/system.yaml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,26 @@ form:
13571357
validate:
13581358
type: bool
13591359

1360+
images.url_actions:
1361+
type: toggle
1362+
label: PLUGIN_ADMIN.IMAGES_URL_ACTIONS
1363+
help: PLUGIN_ADMIN.IMAGES_URL_ACTIONS_HELP
1364+
highlight: 0
1365+
options:
1366+
1: PLUGIN_ADMIN.YES
1367+
0: PLUGIN_ADMIN.NO
1368+
validate:
1369+
type: bool
1370+
1371+
images.max_pixels:
1372+
type: text
1373+
size: small
1374+
label: PLUGIN_ADMIN.IMAGES_MAX_PIXELS
1375+
help: PLUGIN_ADMIN.IMAGES_MAX_PIXELS_HELP
1376+
validate:
1377+
type: int
1378+
min: 0
1379+
13601380
media.enable_media_timestamp:
13611381
type: toggle
13621382
label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP

‎system/config/system.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ images:
164164
debug: false # Show an overlay over images indicating the pixel depth of the image when working with retina for example
165165
auto_fix_orientation: true # Automatically fix the image orientation based on the Exif data
166166
seofriendly: false # SEO-friendly processed image names
167+
url_actions: false # Allow URL query-based image transforms (e.g. `image.jpg?resize=600,400`). Off by default; normal Twig/Markdown resizing is unaffected
168+
max_pixels: 25000000 # Total-pixel ceiling (width*height) for URL-based resize actions. Requests above this are refused. 0 disables the check
167169
cls: # Cumulative Layout Shift: See https://web.dev/optimize-cls/
168170
auto_sizes: false # Automatically add height/width to image
169171
aspect_ratio: false # Reserve space with aspect ratio style

‎system/src/Grav/Common/Grav.php‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -797,11 +797,38 @@ public function fallbackUrl($path)
797797
if (isset($media[$media_file])) {
798798
/** @var Medium $medium */
799799
$medium = $media[$media_file];
800-
foreach ($uri->query(null, true) as $action => $params) {
801-
if (in_array($action, ImageMedium::$magic_actions, true)) {
802-
call_user_func_array([&$medium, $action], explode(',', $params));
800+
801+
// URL-based media actions (e.g. `image.jpg?resize=600,400`) let an
802+
// unauthenticated visitor drive image transforms straight from the
803+
// query string. They are opt-in and disabled by default: the normal
804+
// resize path is Twig/Markdown media methods, which run with
805+
// developer-controlled arguments and are unaffected by this toggle.
806+
if ($config->get('system.images.url_actions', false)) {
807+
$max_pixels = (int) $config->get('system.images.max_pixels', 25000000);
808+
foreach ($uri->query(null, true) as $action => $params) {
809+
if (in_array($action, ImageMedium::$magic_actions, true)) {
810+
$args = explode(',', (string) $params);
811+
// Reject request-derived resize dimensions above the
812+
// total-pixel ceiling. The GD/Imagick output buffer is
813+
// allocated as width*height*4 bytes outside PHP's
814+
// memory_limit, so an unbounded request exhausts RAM.
815+
// The output width/height are the last two positions in
816+
// each $magic_resize_actions entry (crop is x,y,w,h).
817+
if ($max_pixels > 0 && isset(ImageMedium::$magic_resize_actions[$action])) {
818+
$positions = ImageMedium::$magic_resize_actions[$action];
819+
$w_pos = $positions[count($positions) - 2] ?? null;
820+
$h_pos = $positions[count($positions) - 1] ?? null;
821+
$width = ($w_pos !== null && isset($args[$w_pos]) && is_numeric($args[$w_pos])) ? (int) $args[$w_pos] : 0;
822+
$height = ($h_pos !== null && isset($args[$h_pos]) && is_numeric($args[$h_pos])) ? (int) $args[$h_pos] : 0;
823+
if ($width > 0 && $height > 0 && ($width * $height) > $max_pixels) {
824+
return false;
825+
}
826+
}
827+
call_user_func_array([&$medium, $action], $args);
828+
}
803829
}
804830
}
831+
805832
Utils::download($medium->path(), false);
806833
}
807834

0 commit comments

Comments
 (0)