Implement Settings > Media's "Allow SVG uploads" toggle (v2.3.0)
The toggle existed in the UI with no implementation behind it, so uploads were rejected regardless of the setting. New SvgUploads class: - Allowlists the SVG mime type and fixes WP's real-mime sniff, which otherwise nulls out the file type for SVGs specifically even after the mime allowlist is updated. - Populates width/height metadata (parsed from the SVG's own viewBox/width/height) — without this, wp_get_missing_image_subsizes() throws undefined-array-key warnings that can corrupt the upload response's JSON on environments with warnings displayed inline. - Basic sanitization on upload (strips <script> tags, inline event handlers, javascript: URIs). Verified live via the REST media endpoint: clean upload with correct dimensions, and a malicious test SVG had its script/handlers stripped while staying valid markup. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
077e9b8d3c
commit
39af8d650d
@@ -5,6 +5,21 @@ version bump. Starts at 2.1.0 — earlier history (the theming rebuild,
|
||||
window chrome/taskbar, Site Editor support, etc.) lives in `git log` and
|
||||
`PROJECT_STATUS.md`, not reconstructed here.
|
||||
|
||||
## [2.3.0] - 2026-07-04
|
||||
|
||||
### Added
|
||||
- Settings > Media's "Allow SVG uploads" toggle now actually does
|
||||
something — previously existed in the UI with no implementation behind
|
||||
it, so every SVG upload was rejected. Fixes both of WordPress's default
|
||||
blocks (the `upload_mimes` allowlist and the real-mime sniff that nulls
|
||||
out the file type for SVGs specifically), plus a PHP warning
|
||||
(`wp_get_missing_image_subsizes()` reading undefined `width`/`height`
|
||||
metadata keys that SVGs never had) that could otherwise corrupt the
|
||||
upload response on environments with warnings displayed inline. Also
|
||||
does basic sanitization on upload (strips `<script>` tags, inline event
|
||||
handler attributes, `javascript:` URIs) since SVG can carry executable
|
||||
content.
|
||||
|
||||
## [2.2.0] - 2026-07-04
|
||||
|
||||
### Added
|
||||
|
||||
@@ -23,6 +23,7 @@ use AuroraAdmin\ActivityLog\ActivityLog;
|
||||
use AuroraAdmin\ActivityLog\ActivityLogData;
|
||||
use AuroraAdmin\DatabaseExplorer\DatabaseExplorerData;
|
||||
use AuroraAdmin\Login\LoginStyle;
|
||||
use AuroraAdmin\Media\SvgUploads;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
@@ -58,5 +59,6 @@ class AuroraAdmin
|
||||
new ActivityLogData();
|
||||
new DatabaseExplorerData();
|
||||
new LoginStyle();
|
||||
new SvgUploads();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
namespace AuroraAdmin\Media;
|
||||
|
||||
use AuroraAdmin\Options\Settings;
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
/**
|
||||
* Implements Settings > Media's "Allow SVG uploads" toggle — previously
|
||||
* existed in the UI with no backend behind it, so every SVG upload was
|
||||
* rejected by WordPress's default checks regardless of the setting.
|
||||
*
|
||||
* Two separate WP checks both have to pass, not just one:
|
||||
* 1. upload_mimes — the allowlist of extension => mime type pairs.
|
||||
* 2. wp_check_filetype_and_ext()'s real-mime sniff (finfo) — SVG is XML
|
||||
* text, so finfo reports something like text/plain, which doesn't
|
||||
* match image/svg+xml even after (1) allowlists it, and WordPress
|
||||
* nulls out ext/type as a security measure. Fixed here by restoring
|
||||
* them for .svg files specifically, which is the standard, documented
|
||||
* workaround for this exact WP behavior.
|
||||
*
|
||||
* Also does basic sanitization on upload (strips <script> tags, inline
|
||||
* event-handler attributes, javascript: URIs) since SVG can carry
|
||||
* executable content and is a known stored-XSS vector once uploads are
|
||||
* opened up. This is a regex-based pass, not a full XML-parser-based
|
||||
* sanitizer — no Composer dependency in this project to lean on one.
|
||||
* Good enough to block the common attack patterns; not a substitute for
|
||||
* a hardened library (e.g. enshrined/svg-sanitize) if that matters more
|
||||
* than keeping this dependency-free.
|
||||
*/
|
||||
class SvgUploads
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (!Settings::get("enable_svg", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter("upload_mimes", [self::class, "allow_mime"]);
|
||||
add_filter("wp_check_filetype_and_ext", [self::class, "fix_filetype_check"], 10, 4);
|
||||
add_filter("wp_handle_upload_prefilter", [self::class, "sanitize"]);
|
||||
add_filter("wp_generate_attachment_metadata", [self::class, "add_dimensions"], 10, 2);
|
||||
}
|
||||
|
||||
public static function allow_mime($mimes)
|
||||
{
|
||||
$mimes["svg"] = "image/svg+xml";
|
||||
return $mimes;
|
||||
}
|
||||
|
||||
public static function fix_filetype_check($data, $file, $filename, $mimes)
|
||||
{
|
||||
if (!empty($data["ext"]) && !empty($data["type"])) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$wp_filetype = wp_check_filetype($filename, $mimes);
|
||||
if ($wp_filetype["ext"] === "svg" && $wp_filetype["type"] === "image/svg+xml") {
|
||||
$data["ext"] = "svg";
|
||||
$data["type"] = "image/svg+xml";
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function sanitize($file)
|
||||
{
|
||||
if (!isset($file["tmp_name"], $file["name"])) {
|
||||
return $file;
|
||||
}
|
||||
if (strtolower(pathinfo($file["name"], PATHINFO_EXTENSION)) !== "svg") {
|
||||
return $file;
|
||||
}
|
||||
|
||||
$content = file_get_contents($file["tmp_name"]);
|
||||
if ($content === false || stripos($content, "<svg") === false) {
|
||||
$file["error"] = __("File does not appear to be a valid SVG.", "aurora-admin");
|
||||
return $file;
|
||||
}
|
||||
|
||||
$clean = preg_replace(
|
||||
[
|
||||
"/<script\b[^>]*>.*?<\/script>/is",
|
||||
'/\son\w+\s*=\s*"[^"]*"/i',
|
||||
"/\son\w+\s*=\s*'[^']*'/i",
|
||||
'/\son\w+\s*=\s*[^\s>]+/i',
|
||||
'/(href|xlink:href)\s*=\s*"javascript:[^"]*"/i',
|
||||
],
|
||||
"",
|
||||
$content
|
||||
);
|
||||
|
||||
file_put_contents($file["tmp_name"], $clean);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* SVGs have no raster dimensions getimagesize()/Imagick/GD can read, so
|
||||
* WordPress's own metadata generation leaves 'width'/'height' out of the
|
||||
* attachment metadata entirely for them. That's not just cosmetic —
|
||||
* wp_get_missing_image_subsizes() (wp-admin/includes/image.php, called
|
||||
* right after generation) unconditionally reads $image_meta['width']/
|
||||
* ['height'] in its no-imagesize-available branch, throwing "Undefined
|
||||
* array key" warnings. On any environment with warnings displayed
|
||||
* inline (common on staging/debug setups), that warning text gets
|
||||
* prepended to the REST/AJAX response body, which the media uploader's
|
||||
* JS can't parse as JSON — surfacing to the user as "upload failed"
|
||||
* with no useful message. Parsing real dimensions out of the SVG here
|
||||
* fixes both the correctness gap and that failure mode.
|
||||
*/
|
||||
public static function add_dimensions($metadata, $attachment_id)
|
||||
{
|
||||
$file = get_attached_file($attachment_id);
|
||||
if (!$file || strtolower(pathinfo($file, PATHINFO_EXTENSION)) !== "svg") {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$size = self::svg_dimensions($file);
|
||||
$metadata["width"] = $size["width"];
|
||||
$metadata["height"] = $size["height"];
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
private static function svg_dimensions($file)
|
||||
{
|
||||
$width = 100;
|
||||
$height = 100;
|
||||
|
||||
$content = file_get_contents($file);
|
||||
if ($content !== false) {
|
||||
if (preg_match('/<svg\b[^>]*\bviewBox=["\']\s*[\d.\-]+[\s,]+[\d.\-]+[\s,]+([\d.\-]+)[\s,]+([\d.\-]+)/i', $content, $m)) {
|
||||
$width = (float) $m[1];
|
||||
$height = (float) $m[2];
|
||||
} elseif (
|
||||
preg_match('/<svg\b[^>]*\bwidth=["\']([\d.]+)/i', $content, $mw) &&
|
||||
preg_match('/<svg\b[^>]*\bheight=["\']([\d.]+)/i', $content, $mh)
|
||||
) {
|
||||
$width = (float) $mw[1];
|
||||
$height = (float) $mh[1];
|
||||
}
|
||||
}
|
||||
|
||||
return ["width" => max(1, (int) round($width)), "height" => max(1, (int) round($height))];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aurora-admin-app",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+3
-2
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Aurora Admin
|
||||
* Plugin URI: https://aurora.co
|
||||
* Description: A modern, fast WordPress admin interface.
|
||||
* Version: 2.2.0
|
||||
* Version: 2.3.0
|
||||
* Author: Aurora
|
||||
* Text Domain: aurora-admin
|
||||
* Domain Path: /languages/
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
defined("ABSPATH") || exit();
|
||||
|
||||
define("AURORA_ADMIN_VERSION", "2.2.0");
|
||||
define("AURORA_ADMIN_VERSION", "2.3.0");
|
||||
define("AURORA_ADMIN_PATH", plugin_dir_path(__FILE__));
|
||||
define("AURORA_ADMIN_URL", plugins_url("aurora-admin/"));
|
||||
|
||||
@@ -44,5 +44,6 @@ require AURORA_ADMIN_PATH . "admin/src/ActivityLog/ActivityLog.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/ActivityLog/ActivityLogData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/DatabaseExplorer/DatabaseExplorerData.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Login/LoginStyle.php";
|
||||
require AURORA_ADMIN_PATH . "admin/src/Media/SvgUploads.php";
|
||||
|
||||
new AuroraAdmin\App\AuroraAdmin();
|
||||
|
||||
Reference in New Issue
Block a user