fix(preview): harden SVG handling and normalize mime type

This commit is contained in:
Ryan
2025-11-29 23:11:50 -05:00
committed by GitHub
parent a50fa30db2
commit f2ce43f18f
6 changed files with 73 additions and 27 deletions

View File

@@ -1,5 +1,16 @@
# Changelog # 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) ## Changes 11/29/2025 (v2.2.2)
release(v2.2.2): feat(folders): show inline folder stats & dates release(v2.2.2): feat(folders): show inline folder stats & dates

View File

@@ -179,9 +179,22 @@ export function buildFileTableRow(file, folderPath) {
const safeUploader = escapeHTML(file.uploader || "Unknown"); const safeUploader = escapeHTML(file.uploader || "Unknown");
let previewButton = ""; 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 = ""; 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 = `<i class="material-icons">image</i>`; previewIcon = `<i class="material-icons">image</i>`;
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) { } else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">videocam</i>`; previewIcon = `<i class="material-icons">videocam</i>`;
@@ -190,14 +203,16 @@ export function buildFileTableRow(file, folderPath) {
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) { } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
previewIcon = `<i class="material-icons">audiotrack</i>`; previewIcon = `<i class="material-icons">audiotrack</i>`;
} }
previewButton = `<button
type="button" previewButton = `
class="btn btn-sm btn-info preview-btn" <button
data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}" type="button"
data-preview-name="${safeFileName}" class="btn btn-sm btn-info preview-btn"
title="${t('preview')}"> data-preview-url="${folderPath + encodeURIComponent(file.name)}?t=${Date.now()}"
${previewIcon} data-preview-name="${safeFileName}"
</button>`; title="${t('preview')}">
${previewIcon}
</button>`;
} }
return ` return `
@@ -242,13 +257,13 @@ export function buildFileTableRow(file, folderPath) {
<i class="material-icons">drive_file_rename_outline</i> <i class="material-icons">drive_file_rename_outline</i>
</button> </button>
<!-- share --> <!-- share -->
<button <button
type="button" type="button"
class="btn btn-secondary btn-sm share-btn ms-1" class="btn btn-secondary btn-sm share-btn ms-1"
data-file="${safeFileName}" data-file="${safeFileName}"
title="${t('share')}"> title="${t('share')}">
<i class="material-icons">share</i> <i class="material-icons">share</i>
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -1954,7 +1954,7 @@ export function renderGalleryView(folder, container) {
// thumbnail // thumbnail
let 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 const cacheKey = previewURL; // include folder & file
if (window.imageCache && window.imageCache[cacheKey]) { if (window.imageCache && window.imageCache[cacheKey]) {
thumbnail = `<img thumbnail = `<img

View File

@@ -120,7 +120,12 @@ export function openShareModal(file, folder) {
} }
/* -------------------------------- Media modal viewer -------------------------------- */ /* -------------------------------- Media modal viewer -------------------------------- */
const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i; // Images that are safe to inline in <img> tags:
const IMG_RE = /\.(jpg|jpeg|png|gif|bmp|webp|ico)$/i;
// SVG handled separately so we *dont* inline it
const SVG_RE = /\.svg$/i;
const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i; const VID_RE = /\.(mp4|mkv|webm|mov|ogv)$/i;
const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i; const AUD_RE = /\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i;
const ARCH_RE = /\.(zip|rar|7z|gz|bz2|xz|tar)$/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 folder = window.currentFolder || 'root';
const name = fileName; const name = fileName;
const lower = (name || '').toLowerCase(); const lower = (name || '').toLowerCase();
const isSvg = SVG_RE.test(lower);
const isImage = IMG_RE.test(lower); const isImage = IMG_RE.test(lower);
const isVideo = VID_RE.test(lower); const isVideo = VID_RE.test(lower);
const isAudio = AUD_RE.test(lower); const isAudio = AUD_RE.test(lower);
setTitle(overlay, name); 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 -------------------- */ /* -------------------- IMAGES -------------------- */
if (isImage) { if (isImage) {

View File

@@ -342,7 +342,8 @@ const translations = {
"owner": "Owner", "owner": "Owner",
"hide_header_zoom_controls": "Hide header zoom controls", "hide_header_zoom_controls": "Hide header zoom controls",
"preview_not_available": "Preview is not available for this file type.", "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: { es: {
"please_log_in_to_continue": "Por favor, inicie sesión para continuar.", "please_log_in_to_continue": "Por favor, inicie sesión para continuar.",

View File

@@ -503,13 +503,13 @@ class FileModel {
if (!preg_match(REGEX_FILE_NAME, $file)) { if (!preg_match(REGEX_FILE_NAME, $file)) {
return ["error" => "Invalid file name."]; return ["error" => "Invalid file name."];
} }
// Determine the real upload directory. // Determine the real upload directory.
$uploadDirReal = realpath(UPLOAD_DIR); $uploadDirReal = realpath(UPLOAD_DIR);
if ($uploadDirReal === false) { if ($uploadDirReal === false) {
return ["error" => "Server misconfiguration."]; return ["error" => "Server misconfiguration."];
} }
// Determine directory based on folder. // Determine directory based on folder.
if (strtolower($folder) === 'root' || trim($folder) === '') { if (strtolower($folder) === 'root' || trim($folder) === '') {
$directory = $uploadDirReal; $directory = $uploadDirReal;
@@ -524,11 +524,11 @@ class FileModel {
return ["error" => "Invalid folder path."]; return ["error" => "Invalid folder path."];
} }
} }
// Build the file path. // Build the file path.
$filePath = $directory . DIRECTORY_SEPARATOR . $file; $filePath = $directory . DIRECTORY_SEPARATOR . $file;
$realFilePath = realpath($filePath); $realFilePath = realpath($filePath);
// Ensure the file exists and is within the allowed directory. // Ensure the file exists and is within the allowed directory.
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) { if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
return ["error" => "Access forbidden."]; return ["error" => "Access forbidden."];
@@ -536,13 +536,19 @@ class FileModel {
if (!file_exists($realFilePath)) { if (!file_exists($realFilePath)) {
return ["error" => "File not found."]; return ["error" => "File not found."];
} }
// Get the MIME type with safe fallback. // Get the MIME type with safe fallback.
$mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null; $mimeType = function_exists('mime_content_type') ? mime_content_type($realFilePath) : null;
if (!$mimeType) { if (!$mimeType) {
$mimeType = 'application/octet-stream'; $mimeType = 'application/octet-stream';
} }
// OPTIONAL: normalize SVG MIME
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
if ($ext === 'svg') {
$mimeType = 'image/svg+xml';
}
return [ return [
"filePath" => $realFilePath, "filePath" => $realFilePath,
"mimeType" => $mimeType "mimeType" => $mimeType