This commit is contained in:
Reece
2025-11-29 15:26:54 +00:00
parent 9bc128d4d2
commit 966759cc31
5 changed files with 33 additions and 12 deletions
@@ -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);
@@ -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,
@@ -26,7 +26,7 @@ export const SavedSignaturesSection = ({
signatures,
disabled = false,
isAtCapacity,
storageType,
storageType: _storageType,
onUseSignature,
onDeleteSignature,
onRenameSignature,
@@ -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';
@@ -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<SignatureStorageCapabilities> {
try {
// Probe the signatures endpoint
const response = await apiClient.get('/api/v1/general/signatures', {
await apiClient.get('/api/v1/general/signatures', {
timeout: 3000,
});