- {previewUrl && (
+ {isValidPreviewUrl && (

Date: Wed, 18 Feb 2026 01:59:08 +0000
Subject: [PATCH 3/3] fix: strict data URL validation in ImageToDataUrl
Addresses CodeQL alert for "DOM text reinterpreted as HTML" by implementing a strict regex validation for the `src` attribute of the preview image.
The regex `^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,` ensures only safe, expected image MIME types are rendered, preventing XSS vectors.
Also includes previously requested features:
- State persistence using `useLocalStorageState`.
- Drag and drop support.
- Monaco Editor integration for output.
- Improved UI with centered input.
Co-authored-by: sabeerbikba <59386700+sabeerbikba@users.noreply.github.com>
---
src/pages/ImageToDataUrl.jsx | 352 +++++++++++++++++++++++------------
1 file changed, 231 insertions(+), 121 deletions(-)
diff --git a/src/pages/ImageToDataUrl.jsx b/src/pages/ImageToDataUrl.jsx
index ec0a0c8..d897a58 100644
--- a/src/pages/ImageToDataUrl.jsx
+++ b/src/pages/ImageToDataUrl.jsx
@@ -1,21 +1,26 @@
-import { useState, useRef } from "react";
+import { useRef, useState } from "react";
import { toast } from "react-toastify";
+import Editor from "@monaco-editor/react";
import ToolBoxLayout from "@/common/ToolBoxLayout";
import ToolBox from "@/common/ToolBox";
import Btn from "@/common/BasicBtn";
import CopyBtn from "@/common/CopyBtn";
import cn from "@/utils/cn";
+import useLocalStorageState from "@/hooks/useLocalStorageState";
const ImageToDataUrl = () => {
- const [file, setFile] = useState(null);
- const [previewUrl, setPreviewUrl] = useState("");
- const [dataUrl, setDataUrl] = useState("");
- const [svgUrlEncoded, setSvgUrlEncoded] = useState("");
- const [outputType, setOutputType] = useState("base64"); // 'base64' or 'url-encoded'
- // const [isProcessing, setIsProcessing] = useState(false); // Unused for now as operations are fast enough or async handled simply
+ const [state, setPersistedState] = useLocalStorageState("ImageToDataUrl:state", {
+ fileInfo: null, // { name, size, type }
+ dataUrl: "",
+ svgUrlEncoded: "",
+ outputType: "base64",
+ });
+ const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef(null);
+ const { fileInfo, dataUrl, svgUrlEncoded, outputType } = state;
+
// Constants
const RECOMMENDED_SIZE_BYTES = 200 * 1024; // 200KB
const SUPPORTED_TYPES = [
@@ -27,6 +32,17 @@ const ImageToDataUrl = () => {
"image/svg+xml",
];
+ const updateState = (updates) => {
+ try {
+ // useLocalStorageState likely doesn't support functional updates correctly
+ // so we merge manually with the current state in scope
+ setPersistedState({ ...state, ...updates });
+ } catch (error) {
+ console.error("Failed to save state to local storage:", error);
+ toast.error("Storage full? Failed to save state.");
+ }
+ };
+
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (selectedFile) {
@@ -34,7 +50,21 @@ const ImageToDataUrl = () => {
}
};
- const processFile = (selectedFile) => {
+ const readDataURL = (file) => new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+
+ const readText = (file) => new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = reject;
+ reader.readAsText(file);
+ });
+
+ const processFile = async (selectedFile) => {
if (!SUPPORTED_TYPES.includes(selectedFile.type)) {
toast.error(
"Unsupported file type. Please select PNG, JPG, GIF, WEBP, or SVG."
@@ -42,59 +72,49 @@ const ImageToDataUrl = () => {
return;
}
- // setIsProcessing(true);
- setFile(selectedFile);
+ const newFileInfo = {
+ name: selectedFile.name,
+ size: selectedFile.size,
+ type: selectedFile.type,
+ };
- // Create preview URL
- if (previewUrl) {
- URL.revokeObjectURL(previewUrl);
- }
- const objectUrl = URL.createObjectURL(selectedFile);
- setPreviewUrl(objectUrl);
+ try {
+ const [base64, text] = await Promise.all([
+ readDataURL(selectedFile),
+ selectedFile.type === "image/svg+xml" ? readText(selectedFile) : Promise.resolve(null)
+ ]);
- // Reset output
- setDataUrl("");
- setSvgUrlEncoded("");
- setOutputType("base64");
+ let encoded = "";
+ if (text) {
+ encoded = "data:image/svg+xml," +
+ encodeURIComponent(text)
+ .replace(/'/g, "%27")
+ .replace(/"/g, "%22");
+ }
- // Base64 Reader
- const reader = new FileReader();
- reader.onload = () => {
- setDataUrl(reader.result);
- // setIsProcessing(false);
- };
- reader.onerror = () => {
- toast.error("Error reading file.");
- // setIsProcessing(false);
- };
- reader.readAsDataURL(selectedFile);
-
- // SVG URL Encoded Reader
- if (selectedFile.type === "image/svg+xml") {
- const textReader = new FileReader();
- textReader.onload = () => {
- const content = textReader.result;
- // Basic URL encoding for SVG data URI
- // Using encodeURIComponent but ensuring quotes are safer
- const encoded =
- "data:image/svg+xml," +
- encodeURIComponent(content)
- .replace(/'/g, "%27")
- .replace(/"/g, "%22");
- setSvgUrlEncoded(encoded);
- };
- textReader.readAsText(selectedFile);
+ // We replace the entire state relevant to the file,
+ // effectively resetting any stale parts
+ // But we want to preserve other things? No, new file means new state.
+ setPersistedState({
+ fileInfo: newFileInfo,
+ dataUrl: base64,
+ svgUrlEncoded: encoded,
+ outputType: "base64",
+ });
+
+ } catch (e) {
+ console.error(e);
+ toast.error("Error reading file.");
}
};
const handleClear = () => {
- setFile(null);
- if (previewUrl) {
- URL.revokeObjectURL(previewUrl);
- }
- setPreviewUrl("");
- setDataUrl("");
- setSvgUrlEncoded("");
+ setPersistedState({
+ fileInfo: null,
+ dataUrl: "",
+ svgUrlEncoded: "",
+ outputType: "base64",
+ });
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
@@ -108,93 +128,166 @@ const ImageToDataUrl = () => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
- const isLargeFile = file && file.size > RECOMMENDED_SIZE_BYTES;
- const isSvg = file && file.type === "image/svg+xml";
+ // Drag and Drop Handlers
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setIsDragOver(true);
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setIsDragOver(false);
+ const files = e.dataTransfer.files;
+ if (files && files.length > 0) {
+ processFile(files[0]);
+ }
+ };
+
+ const isLargeFile = fileInfo && fileInfo.size > RECOMMENDED_SIZE_BYTES;
+ const isSvg = fileInfo && fileInfo.type === "image/svg+xml";
const currentOutput =
outputType === "url-encoded" && isSvg ? svgUrlEncoded : dataUrl;
- const isValidPreviewUrl = previewUrl && previewUrl.startsWith("blob:");
+ // Validation for preview source using strict regex
+ const isValidDataUrl = (url) => {
+ if (!url) return false;
+ // Strictly allow only image types defined in SUPPORTED_TYPES
+ // Pattern: ^data:image/(png|jpeg|jpg|gif|webp|svg\+xml);base64,
+ const pattern = /^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,/;
+ return pattern.test(url);
+ };
+
+ const isValidPreview = isValidDataUrl(dataUrl);
return (
-
-
-
-
-
- Supported formats: PNG, JPG, GIF, WEBP, SVG
-
-
- Recommended size: < 200KB
-
-
fileInputRef.current?.click()}
- classNames="!w-40"
+
+ {/* Input Area */}
+ {!fileInfo ? (
+
fileInputRef.current?.click()}
+ >
+
+
+
+ Drag & Drop or Click to Select Image
+
+
+ Supported: PNG, JPG, GIF, WEBP, SVG
+
+
+ Recommended size: < 200KB
+
+
{
+ e.stopPropagation();
+ fileInputRef.current?.click();
+ }}
+ classNames="!w-40 mt-2"
+ />
+
-
-
- {file && (
-
-
- {isValidPreviewUrl && (
-

+ ) : (
+ <>
+
-
-
-
Image Info
-
-
Name:
-
{file.name}
-
-
Size:
-
- {formatFileSize(file.size)}
+ onDrop={handleDrop}
+ >
+
+
+ Drag & drop to replace or
+
fileInputRef.current?.click()}
+ classNames="!w-40"
+ />
+
-
Type:
-
{file.type}
+
+
+ {isValidPreview && (
+

+ )}
- {isLargeFile && (
-
- Warning: Image is larger than recommended (200KB). Data URLs can significantly increase file size and affect page load performance.
+
+
Image Info
+
+
Name:
+
{fileInfo.name}
+
+
Size:
+
+ {formatFileSize(fileInfo.size)}
+
+
+
Type:
+
{fileInfo.type}
- )}
-
+ {isLargeFile && (
+
+ Warning: Image is larger than recommended (200KB). Data URLs can significantly increase file size and affect page load performance.
+
+ )}
+
+
+
-
+ >
)}
- {file ? (
+ {fileInfo ? (
<>
{isSvg && (
)}
-
-
@@ -236,8 +335,19 @@ const ImageToDataUrl = () => {
>
) : (
-
- Upload an image to generate Data URL
+
+
)}