diff --git a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java index 3e427acf9..fe5b30ce6 100644 --- a/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java +++ b/app/common/src/main/java/stirling/software/common/service/MobileScannerService.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -25,6 +26,9 @@ import lombok.extern.slf4j.Slf4j; public class MobileScannerService { private static final long SESSION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes + private static final Pattern FILENAME_SANITIZE_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]"); + private static final Pattern SESSION_ID_VALIDATION_PATTERN = Pattern.compile("[a-zA-Z0-9-]+"); + private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$"); private final Map activeSessions = new ConcurrentHashMap<>(); private final Path tempDirectory; @@ -121,7 +125,8 @@ public class MobileScannerService { // Handle duplicate filenames int counter = 1; while (Files.exists(filePath)) { - String nameWithoutExt = safeFilename.replaceFirst("[.][^.]+$", ""); + String nameWithoutExt = + FILE_EXTENSION_PATTERN.matcher(safeFilename).replaceFirst(""); String ext = safeFilename.contains(".") ? safeFilename.substring(safeFilename.lastIndexOf(".")) @@ -271,14 +276,14 @@ public class MobileScannerService { throw new IllegalArgumentException("Session ID cannot be empty"); } // Basic validation: alphanumeric and hyphens only - if (!sessionId.matches("[a-zA-Z0-9-]+")) { + if (!SESSION_ID_VALIDATION_PATTERN.matcher(sessionId).matches()) { throw new IllegalArgumentException("Invalid session ID format"); } } private String sanitizeFilename(String filename) { // Remove path traversal attempts and dangerous characters - String sanitized = filename.replaceAll("[^a-zA-Z0-9._-]", "_"); + String sanitized = FILENAME_SANITIZE_PATTERN.matcher(filename).replaceAll("_"); // Ensure we have a non-empty, safe filename if (sanitized.isBlank()) { sanitized = "upload-" + System.currentTimeMillis(); diff --git a/app/common/src/main/java/stirling/software/common/util/SvgSanitizer.java b/app/common/src/main/java/stirling/software/common/util/SvgSanitizer.java index dbee89764..8841db360 100644 --- a/app/common/src/main/java/stirling/software/common/util/SvgSanitizer.java +++ b/app/common/src/main/java/stirling/software/common/util/SvgSanitizer.java @@ -47,6 +47,7 @@ public class SvgSanitizer { private static final Pattern DATA_SCRIPT_PATTERN = Pattern.compile( "^\\s*data\\s*:[^,]*(?:script|javascript|vbscript)", Pattern.CASE_INSENSITIVE); + private static final Pattern NULL_BYTE_PATTERN = Pattern.compile("\u0000"); private final SsrfProtectionService ssrfProtectionService; private final ApplicationProperties applicationProperties; @@ -210,7 +211,7 @@ public class SvgSanitizer { String result = url.trim(); - result = result.replaceAll("\u0000", ""); + result = NULL_BYTE_PATTERN.matcher(result).replaceAll(""); for (int i = 0; i < 3; i++) { try { diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index bce73b25c..525b61e4d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.regex.Pattern; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -38,6 +39,10 @@ import stirling.software.common.model.ApplicationProperties; }) public class SPDFApplication { + private static final Pattern PORT_SUFFIX_PATTERN = Pattern.compile(".+:\\d+$"); + private static final Pattern URL_SCHEME_PATTERN = + Pattern.compile("^[a-zA-Z][a-zA-Z0-9+.-]*://.*"); + private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/+$"); private static String serverPortStatic; private static String baseUrlStatic; private static String contextPathStatic; @@ -244,8 +249,8 @@ public class SPDFApplication { String trimmedBase = (backendUrl == null || backendUrl.isBlank()) ? "http://localhost" - : backendUrl.trim().replaceAll("/+$", ""); - boolean hasScheme = trimmedBase.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*"); + : TRAILING_SLASH_PATTERN.matcher(backendUrl.trim()).replaceAll(""); + boolean hasScheme = URL_SCHEME_PATTERN.matcher(trimmedBase).matches(); String baseForParsing = hasScheme ? trimmedBase : "http://" + trimmedBase; Integer parsedPort = parsePort(port); @@ -298,7 +303,7 @@ public class SPDFApplication { if (port == null) { return trimmedBase; } - if (trimmedBase.matches(".+:\\d+$")) { + if (PORT_SUFFIX_PATTERN.matcher(trimmedBase).matches()) { return trimmedBase; } return trimmedBase + ":" + port; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index b93f5365a..be5411bc1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.regex.Pattern; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; @@ -51,6 +52,7 @@ import stirling.software.common.util.WebResponseUtils; @RequiredArgsConstructor public class MergeController { + private static final Pattern QUOTE_WRAP_PATTERN = Pattern.compile("^\"|\"$"); private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; @@ -173,7 +175,7 @@ public class MergeController { String[] parts = inside.split(","); String[] result = new String[parts.length]; for (int i = 0; i < parts.length; i++) { - result[i] = parts[i].trim().replaceAll("^\"|\"$", ""); + result[i] = QUOTE_WRAP_PATTERN.matcher(parts[i].trim()).replaceAll(""); } return result; } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java index e1ea2c114..e686778b1 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java @@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters; import java.util.Optional; import java.util.UUID; +import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; @RequiredArgsConstructor public class ConvertPdfJsonController { + private static final Pattern FILE_EXTENSION_PATTERN = Pattern.compile("[.][^.]+$"); private final PdfJsonConversionService pdfJsonConversionService; @Autowired(required = false) @@ -59,7 +61,9 @@ public class ConvertPdfJsonController { String originalName = inputFile.getOriginalFilename(); String baseName = (originalName != null && !originalName.isBlank()) - ? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "") + ? FILE_EXTENSION_PATTERN + .matcher(Filenames.toSimpleFileName(originalName)) + .replaceFirst("") : "document"; String docName = baseName + ".json"; return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); @@ -82,7 +86,9 @@ public class ConvertPdfJsonController { String originalName = jsonFile.getOriginalFilename(); String baseName = (originalName != null && !originalName.isBlank()) - ? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "") + ? FILE_EXTENSION_PATTERN + .matcher(Filenames.toSimpleFileName(originalName)) + .replaceFirst("") : "document"; String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, docName); @@ -115,7 +121,9 @@ public class ConvertPdfJsonController { String originalName = inputFile.getOriginalFilename(); String baseName = (originalName != null && !originalName.isBlank()) - ? Filenames.toSimpleFileName(originalName).replaceFirst("[.][^.]+$", "") + ? FILE_EXTENSION_PATTERN + .matcher(Filenames.toSimpleFileName(originalName)) + .replaceFirst("") : "document"; String docName = baseName + "_metadata.json"; @@ -152,7 +160,9 @@ public class ConvertPdfJsonController { String baseName = (filename != null && !filename.isBlank()) - ? Filenames.toSimpleFileName(filename).replaceFirst("[.][^.]+$", "") + ? FILE_EXTENSION_PATTERN + .matcher(Filenames.toSimpleFileName(filename)) + .replaceFirst("") : Optional.ofNullable(document.getMetadata()) .map(PdfJsonMetadata::getTitle) .filter(title -> title != null && !title.isBlank()) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 9a8021bdc..8b0af3156 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -57,6 +57,7 @@ import stirling.software.common.util.WebResponseUtils; @RequiredArgsConstructor public class StampController { + private static final Pattern NEWLINE_PATTERN = Pattern.compile("\\r?\\n"); private final CustomPDFDocumentFactory pdfDocumentFactory; private final TempFileManager tempFileManager; @@ -266,7 +267,7 @@ public class StampController { .getEscapedNewlinePattern() .matcher(processedStampText) .replaceAll("\n"); - String[] lines = normalizedText.split("\\r?\\n"); + String[] lines = NEWLINE_PATTERN.split(normalizedText); PDRectangle pageSize = page.getMediaBox(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index 83450088e..8093f2fef 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.springframework.core.io.Resource; @@ -44,6 +45,7 @@ import stirling.software.common.util.FileMonitor; public class PipelineDirectoryProcessor { private static final int MAX_DIRECTORY_DEPTH = 50; // Prevent excessive recursion + private static final Pattern WATCHED_FOLDERS_PATTERN = Pattern.compile("\\\\?watchedFolders"); private final ObjectMapper objectMapper; private final ApiDocService apiDocService; @@ -433,10 +435,12 @@ public class PipelineDirectoryProcessor { private Path determineOutputPath(PipelineConfig config, Path dir) { String outputDir = - config.getOutputDir() - .replace("{outputFolder}", finishedFoldersDir) - .replace("{folderName}", dir.toString()) - .replaceAll("\\\\?watchedFolders", ""); + WATCHED_FOLDERS_PATTERN + .matcher( + config.getOutputDir() + .replace("{outputFolder}", finishedFoldersDir) + .replace("{folderName}", dir.toString())) + .replaceAll(""); return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 027b466c0..e903ed8e5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; @@ -28,6 +29,8 @@ public class ReactRoutingController { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ReactRoutingController.class); + private static final Pattern BASE_HREF_PATTERN = + Pattern.compile(""); @Value("${server.servlet.context-path:/}") private String contextPath; @@ -94,9 +97,9 @@ public class ReactRoutingController { html = html.replace("%BASE_URL%", baseUrl); // Also rewrite any existing tag (Vite may have baked one in) html = - html.replaceFirst( - "", - ""); + BASE_HREF_PATTERN + .matcher(html) + .replaceFirst(""); // Inject context path as a global variable for API calls String contextPathScript = diff --git a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java index 85d7ac80c..1d1e5ae8d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonConversionService.java @@ -38,6 +38,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.imageio.ImageIO; @@ -133,6 +134,9 @@ import stirling.software.common.util.TempFileManager; @RequiredArgsConstructor public class PdfJsonConversionService { + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + private static final Pattern WHITESPACE_DASH_UNDERSCORE_PATTERN = Pattern.compile("[\\s\\-_]"); + private static final Pattern FONT_SUBSET_PREFIX_PATTERN = Pattern.compile("^[A-Z]{6}\\+"); private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; private final EndpointConfiguration endpointConfiguration; @@ -531,7 +535,10 @@ public class PdfJsonConversionService { : "Unknown"; // Clean up subset prefix (e.g., "ABCDEF+TimesNewRoman" // -> "TimesNewRoman") - String cleanName = name.replaceAll("^[A-Z]{6}\\+", ""); + String cleanName = + FONT_SUBSET_PREFIX_PATTERN + .matcher(name) + .replaceAll(""); return String.format("%s (%s)", cleanName, subtype); }) .collect(java.util.stream.Collectors.toList()); @@ -1442,9 +1449,9 @@ public class PdfJsonConversionService { if (!fallbackFontService.canEncodeFully(font, text)) { String fontName = fontModel != null && fontModel.getBaseName() != null - ? fontModel - .getBaseName() - .replaceAll("^[A-Z]{6}\\+", "") // Remove subset prefix + ? FONT_SUBSET_PREFIX_PATTERN + .matcher(fontModel.getBaseName()) + .replaceAll("") // Remove subset prefix : (font != null ? font.getName() : "unknown"); String fontKey = fontName + ":" + element.getFontId() + ":" + pageNumber; if (!warnedFonts.contains(fontKey)) { @@ -1902,7 +1909,10 @@ public class PdfJsonConversionService { if (plusIndex >= 0 && plusIndex < normalized.length() - 1) { normalized = normalized.substring(plusIndex + 1); } - normalized = normalized.toLowerCase(Locale.ROOT).replaceAll("[\\s\\-_]", ""); + normalized = + WHITESPACE_DASH_UNDERSCORE_PATTERN + .matcher(normalized.toLowerCase(Locale.ROOT)) + .replaceAll(""); // Exact match after normalization try { @@ -3256,7 +3266,7 @@ public class PdfJsonConversionService { if (value == null) { return ""; } - String trimmed = value.replaceAll("\s+", " ").trim(); + String trimmed = WHITESPACE_PATTERN.matcher(value).replaceAll(" ").trim(); if (trimmed.length() <= 32) { return trimmed; } diff --git a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonFallbackFontService.java b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonFallbackFontService.java index e4baee055..7bd56e700 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonFallbackFontService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/PdfJsonFallbackFontService.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.font.PDFont; @@ -310,6 +311,11 @@ public class PdfJsonFallbackFontService { "classpath:/static/fonts/DejaVuSansMono-BoldOblique.ttf", "DejaVuSansMono-BoldOblique", "ttf"))); + private static final Pattern BOLD_FONT_WEIGHT_PATTERN = + Pattern.compile(".*[_-]?[6-9]00(wght)?.*"); + private static final Pattern FONT_NAME_DELIMITER_PATTERN = Pattern.compile("[-_,+]"); + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + private static final Pattern PATTERN = Pattern.compile("^[A-Z]{6}\\+"); private final ResourceLoader resourceLoader; private final stirling.software.common.model.ApplicationProperties applicationProperties; @@ -418,16 +424,17 @@ public class PdfJsonFallbackFontService { // Normalize font name: remove subset prefix (e.g. "PXAAAC+"), convert to lowercase, // remove spaces String normalized = - originalFontName - .replaceAll("^[A-Z]{6}\\+", "") // Remove subset prefix - .toLowerCase() - .replaceAll("\\s+", ""); // Remove spaces (e.g. "Times New Roman" -> + WHITESPACE_PATTERN + .matcher( + PATTERN.matcher(originalFontName).replaceAll("") // Remove subset prefix + .toLowerCase()) + .replaceAll(""); // Remove spaces (e.g. "Times New Roman" -> // "timesnewroman") // Extract base name without weight/style suffixes // Split on common delimiters: hyphen, underscore, comma, plus // Handles: "Arimo_700wght" -> "arimo", "Arial-Bold" -> "arial", "Arial,Bold" -> "arial" - String baseName = normalized.split("[-_,+]")[0]; + String baseName = FONT_NAME_DELIMITER_PATTERN.split(normalized)[0]; String aliasedFontId = FONT_NAME_ALIASES.get(baseName); if (aliasedFontId != null) { @@ -470,7 +477,7 @@ public class PdfJsonFallbackFontService { // Check for numeric weight indicators (600-900 = bold) // Handles: "Arimo_700wght", "Arial-700", "Font-w700" - if (normalizedFontName.matches(".*[_-]?[6-9]00(wght)?.*")) { + if (BOLD_FONT_WEIGHT_PATTERN.matcher(normalizedFontName).matches()) { return true; } @@ -514,7 +521,7 @@ public class PdfJsonFallbackFontService { // Supported: Liberation (Sans/Serif/Mono), Noto Sans, DejaVu (Sans/Serif/Mono) boolean isSupported = baseFontId.startsWith("fallback-liberation-") - || baseFontId.equals("fallback-noto-sans") + || "fallback-noto-sans".equals(baseFontId) || baseFontId.startsWith("fallback-dejavu-"); if (!isSupported) { @@ -523,8 +530,8 @@ public class PdfJsonFallbackFontService { // DejaVu Sans and Mono use "oblique" instead of "italic" boolean useOblique = - baseFontId.equals("fallback-dejavu-sans") - || baseFontId.equals("fallback-dejavu-mono"); + "fallback-dejavu-sans".equals(baseFontId) + || "fallback-dejavu-mono".equals(baseFontId); if (isBold && isItalic) { return baseFontId + (useOblique ? "-boldoblique" : "-bolditalic"); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java index 7043118a4..53d43d26c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SharedSignatureService.java @@ -9,6 +9,7 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.springframework.stereotype.Service; @@ -27,6 +28,7 @@ import stirling.software.common.configuration.InstallationPathConfig; @Slf4j public class SharedSignatureService { + private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$"); private final String SIGNATURE_BASE_PATH; private final String ALL_USERS_FOLDER = "ALL_USERS"; private final ObjectMapper objectMapper; @@ -105,7 +107,7 @@ public class SharedSignatureService { throw new IllegalArgumentException("Invalid filename"); } // Only allow alphanumeric, hyphen, underscore, and dot (for extensions) - if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) { + if (!FILENAME_VALIDATION_PATTERN.matcher(fileName).matches()) { throw new IllegalArgumentException("Filename contains invalid characters"); } } @@ -113,7 +115,7 @@ public class SharedSignatureService { 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")) { + if ("png".equals(normalized) || "jpg".equals(normalized) || "jpeg".equals(normalized)) { return normalized; } throw new IllegalArgumentException("Unsupported image extension: " + extension); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index 5a8e42401..c502449e8 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -18,6 +18,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.List; +import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.junit.jupiter.api.AfterEach; @@ -46,6 +47,7 @@ import stirling.software.common.util.WebResponseUtils; public class ConvertWebsiteToPdfTest { + private static final Pattern PDF_FILENAME_PATTERN = Pattern.compile("[A-Za-z0-9_]+\\.pdf"); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -142,7 +144,7 @@ public class ConvertWebsiteToPdfTest { assertTrue(out.endsWith(".pdf")); // Only A–Z, a–z, 0–9, underscore and dot allowed - assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf")); + assertTrue(PDF_FILENAME_PATTERN.matcher(out).matches()); // no truncation here (source not that long) assertTrue(out.length() <= 54); } @@ -159,7 +161,7 @@ public class ConvertWebsiteToPdfTest { String out = (String) m.invoke(sut, longUrl); assertTrue(out.endsWith(".pdf")); - assertTrue(out.matches("[A-Za-z0-9_]+\\.pdf")); + assertTrue(PDF_FILENAME_PATTERN.matcher(out).matches()); // safeName limited to 50 -> total max 54 including '.pdf' assertTrue(out.length() <= 54, "Filename should be truncated to 50 + '.pdf'"); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java index 3d6359538..158abf486 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/StampControllerTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.LocalDateTime; +import java.util.regex.Pattern; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; @@ -26,6 +27,18 @@ import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class StampControllerTest { + private static final Pattern UUID_HEX_PATTERN = Pattern.compile("[0-9a-f]{8}"); + private static final Pattern DATE_LITERAL_REGEX = + Pattern.compile("@date is \\d{4}-\\d{2}-\\d{2}"); + private static final Pattern DATE_TIME_MIN_PATTERN = + Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"); + private static final Pattern DATE_SLASH_PATTERN = Pattern.compile("\\d{2}/\\d{2}/\\d{4}"); + private static final Pattern DAY_LABEL_PATTERN = Pattern.compile("Day: \\d{2}"); + private static final Pattern MONTH_LABEL_PATTERN = Pattern.compile("Month: \\d{2}"); + private static final Pattern DATE_TIME_FULL_PATTERN = + Pattern.compile("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); + private static final Pattern TIME_LABEL_PATTERN = Pattern.compile("Time: \\d{2}:\\d{2}:\\d{2}"); + private static final Pattern DATE_LABEL_PATTERN = Pattern.compile("Date: \\d{4}-\\d{2}-\\d{2}"); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; @@ -173,7 +186,7 @@ class StampControllerTest { void testDateReplacement() throws Exception { String result = invokeProcessStampText("Date: @date", 1, 1, "test.pdf", null); assertTrue( - result.matches("Date: \\d{4}-\\d{2}-\\d{2}"), + DATE_LABEL_PATTERN.matcher(result).matches(), "Date should match YYYY-MM-DD format"); } @@ -182,7 +195,7 @@ class StampControllerTest { void testTimeReplacement() throws Exception { String result = invokeProcessStampText("Time: @time", 1, 1, "test.pdf", null); assertTrue( - result.matches("Time: \\d{2}:\\d{2}:\\d{2}"), + TIME_LABEL_PATTERN.matcher(result).matches(), "Time should match HH:mm:ss format"); } @@ -192,7 +205,7 @@ class StampControllerTest { String result = invokeProcessStampText("@datetime", 1, 1, "test.pdf", null); // DateTime format: YYYY-MM-DD HH:mm:ss assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"), + DATE_TIME_FULL_PATTERN.matcher(result).matches(), "DateTime should match YYYY-MM-DD HH:mm:ss format"); } @@ -208,14 +221,15 @@ class StampControllerTest { @DisplayName("Should replace @month with zero-padded month") void testMonthReplacement() throws Exception { String result = invokeProcessStampText("Month: @month", 1, 1, "test.pdf", null); - assertTrue(result.matches("Month: \\d{2}"), "Month should be zero-padded"); + assertTrue( + MONTH_LABEL_PATTERN.matcher(result).matches(), "Month should be zero-padded"); } @Test @DisplayName("Should replace @day with zero-padded day") void testDayReplacement() throws Exception { String result = invokeProcessStampText("Day: @day", 1, 1, "test.pdf", null); - assertTrue(result.matches("Day: \\d{2}"), "Day should be zero-padded"); + assertTrue(DAY_LABEL_PATTERN.matcher(result).matches(), "Day should be zero-padded"); } } @@ -228,7 +242,7 @@ class StampControllerTest { void testCustomDateFormatSlash() throws Exception { String result = invokeProcessStampText("@date{dd/MM/yyyy}", 1, 1, "test.pdf", null); assertTrue( - result.matches("\\d{2}/\\d{2}/\\d{4}"), + DATE_SLASH_PATTERN.matcher(result).matches(), "Should match dd/MM/yyyy format: " + result); } @@ -238,7 +252,7 @@ class StampControllerTest { String result = invokeProcessStampText("@date{yyyy-MM-dd HH:mm}", 1, 1, "test.pdf", null); assertTrue( - result.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"), + DATE_TIME_MIN_PATTERN.matcher(result).matches(), "Should match yyyy-MM-dd HH:mm format: " + result); } @@ -345,7 +359,7 @@ class StampControllerTest { // @@date should become @date, and @date should be replaced with actual date assertTrue(result.startsWith("@date is "), "Should start with literal @date"); assertTrue( - result.matches("@date is \\d{4}-\\d{2}-\\d{2}"), + DATE_LITERAL_REGEX.matcher(result).matches(), "Should have date after: " + result); } @@ -463,7 +477,9 @@ class StampControllerTest { @DisplayName("UUID should contain only hex characters") void testUuidFormat() throws Exception { String result = invokeProcessStampText("@uuid", 1, 1, "test.pdf", null); - assertTrue(result.matches("[0-9a-f]{8}"), "UUID should be 8 hex characters: " + result); + assertTrue( + UUID_HEX_PATTERN.matcher(result).matches(), + "UUID should be 8 hex characters: " + result); } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java index baa5082fe..3af99ab60 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.*; +import java.util.regex.Pattern; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -36,6 +37,7 @@ import stirling.software.common.configuration.RuntimePathConfig; @RequiredArgsConstructor public class UIDataTessdataController { + private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]"); private final RuntimePathConfig runtimePathConfig; private static volatile List cachedRemoteTessdata = null; private static volatile long cachedRemoteTessdataExpiry = 0L; @@ -88,7 +90,7 @@ public class UIDataTessdataController { failed.add(language); continue; } - String safeLang = language.replaceAll("[^A-Za-z0-9_+\\-]", ""); + String safeLang = INVALID_LANG_CHARS_PATTERN.matcher(language).replaceAll(""); if (!safeLang.equals(language)) { failed.add(language); continue; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/TotpService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/TotpService.java index 7d64de445..ea302dcec 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/TotpService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/TotpService.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; import java.time.Instant; +import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -33,6 +34,7 @@ public class TotpService { private static final String HMAC_ALGORITHM = "HmacSHA1"; private static final String DEFAULT_ISSUER = "Stirling PDF"; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final Pattern TOTP_CODE_PATTERN = Pattern.compile("\\d{6}"); private final ApplicationProperties applicationProperties; @@ -71,7 +73,7 @@ public class TotpService { } String normalizedCode = code.replace(" ", ""); - if (!normalizedCode.matches("\\d{6}")) { + if (!TOTP_CODE_PATTERN.matcher(normalizedCode).matches()) { return null; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java index 63ee28512..e4956b37f 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/service/SignatureService.java @@ -10,6 +10,7 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.springframework.stereotype.Service; @@ -32,6 +33,7 @@ import stirling.software.proprietary.model.api.signature.SavedSignatureResponse; @Slf4j public class SignatureService implements PersonalSignatureServiceInterface { + private static final Pattern FILENAME_VALIDATION_PATTERN = Pattern.compile("^[a-zA-Z0-9_.-]+$"); private final String SIGNATURE_BASE_PATH; private final String ALL_USERS_FOLDER = "ALL_USERS"; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -366,14 +368,14 @@ public class SignatureService implements PersonalSignatureServiceInterface { if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) { throw new IllegalArgumentException("Invalid filename"); } - if (!fileName.matches("^[a-zA-Z0-9_.-]+$")) { + if (!FILENAME_VALIDATION_PATTERN.matcher(fileName).matches()) { throw new IllegalArgumentException("Filename contains invalid characters"); } } private String validateAndNormalizeExtension(String extension) { String normalized = extension.toLowerCase().trim(); - if (normalized.equals("png") || normalized.equals("jpg") || normalized.equals("jpeg")) { + if ("png".equals(normalized) || "jpg".equals(normalized) || "jpeg".equals(normalized)) { return normalized; } throw new IllegalArgumentException("Unsupported image extension: " + extension); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/TotpServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/TotpServiceTest.java index 1e9d1d26e..9b95cc4b6 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/service/TotpServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/service/TotpServiceTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; @@ -16,6 +17,8 @@ import stirling.software.proprietary.security.util.Base32Codec; class TotpServiceTest { + private static final Pattern PATTERN = Pattern.compile("[A-Z2-7]+"); + private TotpService buildService(String appName) { ApplicationProperties properties = new ApplicationProperties(); ApplicationProperties.Ui ui = new ApplicationProperties.Ui(); @@ -32,7 +35,7 @@ class TotpServiceTest { assertNotNull(secret); assertEquals(32, secret.length()); - assertTrue(secret.matches("[A-Z2-7]+")); + assertTrue(PATTERN.matcher(secret).matches()); } @Test