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:
nixpanel
2026-07-04 07:46:56 -05:00
co-authored by Claude Sonnet 5
parent 077e9b8d3c
commit 39af8d650d
5 changed files with 168 additions and 3 deletions
+15
View File
@@ -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
+2
View File
@@ -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();
}
}
+147
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"name": "aurora-admin-app",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"type": "module",
"scripts": {
+3 -2
View File
@@ -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();