diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java index 3e259ab312..d66129852c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java @@ -53,8 +53,6 @@ public class SignatureController { String lowerFileName = fileName.toLowerCase(); if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) { contentType = MediaType.IMAGE_JPEG; - } else if (lowerFileName.endsWith(".gif")) { - contentType = MediaType.IMAGE_GIF; } return ResponseEntity.ok().contentType(contentType).body(imageBytes); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java index 5e1e4a50fb..326b041a4c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java @@ -101,16 +101,35 @@ public class SignatureService { private boolean isImageFile(Path path) { String fileName = path.getFileName().toString().toLowerCase(); - return fileName.endsWith(".jpg") - || fileName.endsWith(".jpeg") - || fileName.endsWith(".png") - || fileName.endsWith(".gif"); + return fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") || fileName.endsWith(".png"); } private void validateFileName(String fileName) { if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) { throw new IllegalArgumentException("Invalid filename"); } + // Only allow alphanumeric, hyphen, and underscore + if (!fileName.matches("^[a-zA-Z0-9_-]+$")) { + throw new IllegalArgumentException("Filename contains invalid characters"); + } + } + + private String validateAndNormalizeExtension(String extension) { + String normalized = extension.toLowerCase().trim(); + // Whitelist only safe image extensions + if (normalized.equals("png") || normalized.equals("jpg") || normalized.equals("jpeg")) { + return normalized; + } + throw new IllegalArgumentException("Unsupported image extension: " + extension); + } + + private void verifyPathWithinDirectory(Path resolvedPath, Path targetDirectory) + throws IOException { + Path canonicalTarget = targetDirectory.toAbsolutePath().normalize(); + Path canonicalResolved = resolvedPath.toAbsolutePath().normalize(); + if (!canonicalResolved.startsWith(canonicalTarget)) { + throw new IOException("Resolved path is outside the target directory"); + } } /** Save a signature as image file */ @@ -145,13 +164,18 @@ public class SignatureService { String base64Data = dataUrl.substring(dataUrl.indexOf(",") + 1); byte[] imageBytes = Base64.getDecoder().decode(base64Data); - // Determine file extension from data URL + // Determine and validate file extension from data URL String mimeType = dataUrl.substring(dataUrl.indexOf(":") + 1, dataUrl.indexOf(";")); - String extension = mimeType.substring(mimeType.indexOf("/") + 1); + String rawExtension = mimeType.substring(mimeType.indexOf("/") + 1); + String extension = validateAndNormalizeExtension(rawExtension); // Save image file only String imageFileName = request.getId() + "." + extension; Path imagePath = targetFolder.resolve(imageFileName); + + // Verify path is within target directory + verifyPathWithinDirectory(imagePath, targetFolder); + Files.write( imagePath, imageBytes, diff --git a/frontend/src/core/components/tools/sign/SavedSignaturesSection.tsx b/frontend/src/core/components/tools/sign/SavedSignaturesSection.tsx index c7eb288ac1..c2e8ca8f1d 100644 --- a/frontend/src/core/components/tools/sign/SavedSignaturesSection.tsx +++ b/frontend/src/core/components/tools/sign/SavedSignaturesSection.tsx @@ -26,7 +26,7 @@ export const SavedSignaturesSection = ({ signatures, disabled = false, isAtCapacity, - storageType, + storageType: _storageType, onUseSignature, onDeleteSignature, onRenameSignature, diff --git a/frontend/src/core/hooks/tools/sign/useSavedSignatures.ts b/frontend/src/core/hooks/tools/sign/useSavedSignatures.ts index 8985c41b5a..655038a3f2 100644 --- a/frontend/src/core/hooks/tools/sign/useSavedSignatures.ts +++ b/frontend/src/core/hooks/tools/sign/useSavedSignatures.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { signatureStorageService, type StorageType } from '@app/services/signatureStorageService'; -const STORAGE_KEY = 'stirling:saved-signatures:v1'; export const MAX_SAVED_SIGNATURES = 10; export type SavedSignatureType = 'canvas' | 'image' | 'text'; diff --git a/frontend/src/core/services/signatureStorageService.ts b/frontend/src/core/services/signatureStorageService.ts index 69c682bf9a..3db6802f4c 100644 --- a/frontend/src/core/services/signatureStorageService.ts +++ b/frontend/src/core/services/signatureStorageService.ts @@ -1,5 +1,5 @@ import apiClient from '@app/services/apiClient'; -import type { SavedSignature, SavedSignaturePayload } from '@app/hooks/tools/sign/useSavedSignatures'; +import type { SavedSignature } from '@app/hooks/tools/sign/useSavedSignatures'; export type StorageType = 'backend' | 'localStorage'; @@ -41,7 +41,7 @@ class SignatureStorageService { private async _performDetection(): Promise { try { // Probe the signatures endpoint - const response = await apiClient.get('/api/v1/general/signatures', { + await apiClient.get('/api/v1/general/signatures', { timeout: 3000, });