diff --git a/CHANGELOG.md b/CHANGELOG.md
index a792d31..cfa5795 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## Changes 11/29/2025 (v2.2.3)
+
+fix(preview): harden SVG handling and normalize mime type
+
+- Stop treating SVGs as inline-previewable images in file list and preview modal
+- Show a clear “SVG preview disabled for security reasons” message instead
+- Keep SVGs downloadable via /api/file/download.php with proper image/svg+xml MIME
+- Add i18n key for svg_preview_disabled
+
+---
+
## Changes 11/29/2025 (v2.2.2)
release(v2.2.2): feat(folders): show inline folder stats & dates
diff --git a/public/js/domUtils.js b/public/js/domUtils.js
index d8749a6..2873b9a 100644
--- a/public/js/domUtils.js
+++ b/public/js/domUtils.js
@@ -179,9 +179,22 @@ export function buildFileTableRow(file, folderPath) {
const safeUploader = escapeHTML(file.uploader || "Unknown");
let previewButton = "";
- if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i.test(file.name)) {
+
+ const isSvg = /\.svg$/i.test(file.name);
+
+ // IMPORTANT: do NOT treat SVG as previewable
+ if (
+ !isSvg &&
+ /\.(jpg|jpeg|png|gif|bmp|webp|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i
+ .test(file.name)
+ ) {
let previewIcon = "";
- if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
+
+ // images (SVG explicitly excluded)
+ if (
+ /\.(jpg|jpeg|png|gif|bmp|webp|ico|tif|tiff|eps|heic)$/i
+ .test(file.name)
+ ) {
previewIcon = `image`;
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
previewIcon = `videocam`;
@@ -190,14 +203,16 @@ export function buildFileTableRow(file, folderPath) {
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
previewIcon = `audiotrack`;
}
- previewButton = ``;
+
+ previewButton = `
+ `;
}
return `
@@ -242,13 +257,13 @@ export function buildFileTableRow(file, folderPath) {
drive_file_rename_outline
-
+
diff --git a/public/js/fileListView.js b/public/js/fileListView.js
index 6589d3d..e04b3b4 100644
--- a/public/js/fileListView.js
+++ b/public/js/fileListView.js
@@ -1954,7 +1954,7 @@ export function renderGalleryView(folder, container) {
// thumbnail
let thumbnail;
- if (/\.(jpe?g|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
+ if (/\.(jpe?g|png|gif|bmp|webp|ico)$/i.test(file.name)) {
const cacheKey = previewURL; // include folder & file
if (window.imageCache && window.imageCache[cacheKey]) {
thumbnail = `
tags:
+const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|ico)$/i;
+
+// SVG handled separately so we *don’t* inline it
+const SVG_RE = /\.svg$/i;
+
const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i;
const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i;
const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/i;
@@ -422,11 +427,19 @@ export function previewFile(fileUrl, fileName) {
const folder = window.currentFolder || 'root';
const name = fileName;
const lower = (name || '').toLowerCase();
+ const isSvg = SVG_RE.test(lower);
const isImage = IMG_RE.test(lower);
const isVideo = VID_RE.test(lower);
const isAudio = AUD_RE.test(lower);
setTitle(overlay, name);
+ if (isSvg) {
+ container.textContent =
+ t("svg_preview_disabled") ||
+ "SVG preview is disabled for security. Use Download to view this file.";
+ overlay.style.display = "flex";
+ return;
+ }
/* -------------------- IMAGES -------------------- */
if (isImage) {
diff --git a/public/js/i18n.js b/public/js/i18n.js
index 22b1e19..316716d 100644
--- a/public/js/i18n.js
+++ b/public/js/i18n.js
@@ -342,7 +342,8 @@ const translations = {
"owner": "Owner",
"hide_header_zoom_controls": "Hide header zoom controls",
"preview_not_available": "Preview is not available for this file type.",
- "storage_pro_bundle_outdated": "Please upgrade to the latest FileRise Pro bundle to use the Storage explorer."
+ "storage_pro_bundle_outdated": "Please upgrade to the latest FileRise Pro bundle to use the Storage explorer.",
+ "svg_preview_disabled": "SVG preview is disabled for now for security reasons."
},
es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.",
diff --git a/src/models/FileModel.php b/src/models/FileModel.php
index 5418db8..9fb7d45 100644
--- a/src/models/FileModel.php
+++ b/src/models/FileModel.php
@@ -503,13 +503,13 @@ class FileModel {
if (!preg_match(REGEX_FILE_NAME, $file)) {
return ["error" => "Invalid file name."];
}
-
+
// Determine the real upload directory.
$uploadDirReal = realpath(UPLOAD_DIR);
if ($uploadDirReal === false) {
return ["error" => "Server misconfiguration."];
}
-
+
// Determine directory based on folder.
if (strtolower($folder) === 'root' || trim($folder) === '') {
$directory = $uploadDirReal;
@@ -524,11 +524,11 @@ class FileModel {
return ["error" => "Invalid folder path."];
}
}
-
+
// Build the file path.
- $filePath = $directory . DIRECTORY_SEPARATOR . $file;
+ $filePath = $directory . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath);
-
+
// Ensure the file exists and is within the allowed directory.
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
return ["error" => "Access forbidden."];
@@ -536,13 +536,19 @@ class FileModel {
if (!file_exists($realFilePath)) {
return ["error" => "File not found."];
}
-
+
// Get the MIME type with safe fallback.
$mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null;
if (!$mimeType) {
$mimeType = 'application/octet-stream';
}
-
+
+ // OPTIONAL: normalize SVG MIME
+ $ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
+ if ($ext === 'svg') {
+ $mimeType = 'image/svg+xml';
+ }
+
return [
"filePath" => $realFilePath,
"mimeType" => $mimeType