From 3c8fb8ac9658fea7652ba150001464bd1005c4d8 Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 13 Jan 2026 14:59:59 +0100 Subject: [PATCH] build(ci): pin base container images, switch npm install to npm ci, and harden EML error handling (#5353) # Description of Changes This pull request introduces several improvements focused on security and reliability in both the Docker build process and the backend API. The most significant changes are the use of digest-pinned Docker base images to ensure reproducible builds, safer handling of user-provided filenames in error messages, and a switch to more reliable dependency installation in CI workflows. **Docker image security and reproducibility:** * All Dockerfiles now use digest-pinned base images (e.g., `node:20-alpine@sha256:...`, `gradle:8.14-jdk21@sha256:...`, `alpine:3.22.1@sha256:...`, `nginx:alpine@sha256:...`) to guarantee build consistency and protect against upstream image changes. [[1]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L5-R5) [[2]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L18-R18) [[3]](diffhunk://#diff-f8faae0938488156cf26e9322ffdf755deaa8770a7ac8c524dd6126c19548888L38-R38) [[4]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L5-R5) [[5]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L18-R18) [[6]](diffhunk://#diff-2f5cd3ad965c86a7a5b4af6e0513ad294e0426644d9f5b5358dfb16a2ef995a7L37-R37) [[7]](diffhunk://#diff-e9edf3a05475d0721a0e65be1ba0eeb162ae972891b0f6d7e1285687efab1de0L9-R9) [[8]](diffhunk://#diff-fa0700cfd7d90d832649eb1d0503904564bb3b28c48972be7d9f17e4ce32a3dcL9-R9) [[9]](diffhunk://#diff-2e766aaf0c87e7b8a62d2a2986f6999c38cc35f677479e31b77d1b427c7aeef7L5-R5) [[10]](diffhunk://#diff-1726db0cbef194c9be3cba9825c0794802b154e15e4c892c1544d0aace03e037L5-R5) [[11]](diffhunk://#diff-c1b6dd504a16fc68cd064baf9cf07d9dd31da56eb55de69601844ab03a5ae319L5-R5) [[12]](diffhunk://#diff-2fc7fcfcfdbb617dd8fbb6b1a2ea5709f9018d618d13942cb33d3e0ed127df16L5-R5) [[13]](diffhunk://#diff-2fc7fcfcfdbb617dd8fbb6b1a2ea5709f9018d618d13942cb33d3e0ed127df16L39-R39) [[14]](diffhunk://#diff-759e94102d21fe6f9bde8ddb0b4f95b5d5cd214b0355ea0419d3ea6c09e8ffbfL2-R2) [[15]](diffhunk://#diff-759e94102d21fe6f9bde8ddb0b4f95b5d5cd214b0355ea0419d3ea6c09e8ffbfL19-R19) **Backend API security:** * In `ConvertEmlToPDF.java`, error messages now escape user-provided filenames using `HtmlUtils.htmlEscape`, preventing potential XSS vulnerabilities when displaying error messages that include filenames. [[1]](diffhunk://#diff-45d22a96bae3e8a746b7fb2c39e25c80aee0bf733b528a3517db8fdd2a3d25cdR13) [[2]](diffhunk://#diff-45d22a96bae3e8a746b7fb2c39e25c80aee0bf733b528a3517db8fdd2a3d25cdR156-R170) **CI/CD reliability:** * All GitHub Actions workflows (`multiOSReleases.yml`, `releaseArtifacts.yml`, `tauri-build.yml`) now use `npm ci` instead of `npm install` for frontend dependency installation, ensuring clean, reproducible installs that match the lockfile. [[1]](diffhunk://#diff-895b214ee023c8c26048a2a3b946cfb1ebc4f26fbc8a9c2fa54b77c12e763b6bL271-R271) [[2]](diffhunk://#diff-699ff98fe113446c403eb07daf16dd1966c2a047ab0b9f7e38fd695d079f7dddL177-R177) [[3]](diffhunk://#diff-b34ab107dd4bc92075b2e89b6f16e4a2813e267ca7c2afebdb1931a0a3900d5aL177-R177) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/workflows/multiOSReleases.yml | 2 +- .github/workflows/tauri-build.yml | 2 +- .../api/converters/ConvertEmlToPDF.java | 15 ++++++++++----- docker/Dockerfile.unified | 6 +++--- docker/Dockerfile.unified-lite | 6 +++--- docker/backend/Dockerfile | 2 +- docker/backend/Dockerfile.fat | 2 +- docker/backend/Dockerfile.ultra-lite | 2 +- docker/embedded/Dockerfile | 2 +- docker/embedded/Dockerfile.fat | 2 +- docker/embedded/Dockerfile.ultra-lite | 4 ++-- docker/frontend/Dockerfile | 4 ++-- 12 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 34f9e6f7f..7c9d42b30 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -268,7 +268,7 @@ jobs: - name: Install frontend dependencies working-directory: ./frontend - run: npm install + run: npm ci # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index b8a899c9b..c3a440b12 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -174,7 +174,7 @@ jobs: - name: Install frontend dependencies working-directory: ./frontend - run: npm install + run: npm ci # DigiCert KeyLocker Setup (Cloud HSM) - name: Setup DigiCert KeyLocker diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index 6a88d8c4f..489978af8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -10,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.HtmlUtils; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -152,20 +153,24 @@ public class ConvertEmlToPDF { } private static @NotNull String buildErrorMessage(Exception e, String originalFilename) { + String safeFilename = HtmlUtils.htmlEscape(originalFilename); + String exceptionMessage = e.getMessage(); + String safeExceptionMessage = + exceptionMessage == null ? "Unknown error" : HtmlUtils.htmlEscape(exceptionMessage); String errorMessage; - if (e.getMessage() != null && e.getMessage().contains("Invalid EML")) { + if (exceptionMessage != null && exceptionMessage.contains("Invalid EML")) { errorMessage = "Invalid EML file format. Please ensure you've uploaded a valid email" + " file (" - + originalFilename + + safeFilename + ")."; - } else if (e.getMessage() != null && e.getMessage().contains("WeasyPrint")) { + } else if (exceptionMessage != null && exceptionMessage.contains("WeasyPrint")) { errorMessage = "PDF generation failed for " - + originalFilename + + safeFilename + ". This may be due to complex email formatting."; } else { - errorMessage = "Conversion failed for " + originalFilename + ": " + e.getMessage(); + errorMessage = "Conversion failed for " + safeFilename + ": " + safeExceptionMessage; } return errorMessage; } diff --git a/docker/Dockerfile.unified b/docker/Dockerfile.unified index a07d294ee..6d045b4f2 100644 --- a/docker/Dockerfile.unified +++ b/docker/Dockerfile.unified @@ -2,7 +2,7 @@ # Supports MODE parameter: BOTH (default), FRONTEND, BACKEND # Stage 1: Build Frontend -FROM node:20-alpine AS frontend-build +FROM node:20-alpine@sha256:658d0f63e501824d6c23e06d4bb95c71e7d704537c9d9272f488ac03a370d448 AS frontend-build WORKDIR /app @@ -15,7 +15,7 @@ COPY frontend . RUN DISABLE_ADDITIONAL_FEATURES=false VITE_API_BASE_URL=/ npm run build # Stage 2: Build Backend (server-only JAR - no UI) -FROM gradle:8.14-jdk21 AS backend-build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS backend-build COPY build.gradle . COPY settings.gradle . @@ -35,7 +35,7 @@ RUN DISABLE_ADDITIONAL_FEATURES=false \ ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube # Stage 3: Final unified image -FROM alpine:3.22.1 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 ARG VERSION_TAG diff --git a/docker/Dockerfile.unified-lite b/docker/Dockerfile.unified-lite index 8219a2173..264d03d67 100644 --- a/docker/Dockerfile.unified-lite +++ b/docker/Dockerfile.unified-lite @@ -2,7 +2,7 @@ # Supports MODE parameter: BOTH (default), FRONTEND, BACKEND # Stage 1: Build Frontend -FROM node:20-alpine AS frontend-build +FROM node:20-alpine@sha256:658d0f63e501824d6c23e06d4bb95c71e7d704537c9d9272f488ac03a370d448 AS frontend-build WORKDIR /app @@ -15,7 +15,7 @@ COPY frontend . RUN DISABLE_ADDITIONAL_FEATURES=true VITE_API_BASE_URL=/ npm run build # Stage 2: Build Backend -FROM gradle:8.14-jdk21 AS backend-build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS backend-build COPY build.gradle . COPY settings.gradle . @@ -34,7 +34,7 @@ RUN DISABLE_ADDITIONAL_FEATURES=true \ ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube # Stage 3: Final unified ultra-lite image -FROM alpine:3.22.1 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 ARG VERSION_TAG diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 45809b096..8ebccfaf7 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -6,7 +6,7 @@ # ======================================== # STAGE 1: Build stage - Alpine with Gradle # ======================================== -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build COPY build.gradle . COPY settings.gradle . diff --git a/docker/backend/Dockerfile.fat b/docker/backend/Dockerfile.fat index 37a99add8..8e31c27c0 100644 --- a/docker/backend/Dockerfile.fat +++ b/docker/backend/Dockerfile.fat @@ -6,7 +6,7 @@ # ======================================== # STAGE 1: Build stage - Gradle # ======================================== -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build COPY build.gradle . COPY settings.gradle . diff --git a/docker/backend/Dockerfile.ultra-lite b/docker/backend/Dockerfile.ultra-lite index 813a01d73..2bf85f7a8 100644 --- a/docker/backend/Dockerfile.ultra-lite +++ b/docker/backend/Dockerfile.ultra-lite @@ -2,7 +2,7 @@ # ======================================== # STAGE 1: Build stage - Gradle # ======================================== -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build COPY build.gradle . COPY settings.gradle . diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 515fdf1b9..dd2124b18 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -2,7 +2,7 @@ # Single JAR contains both frontend and backend # Stage 1: Build application with embedded frontend -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build # Install Node.js and npm for frontend build RUN apt-get update && apt-get install -y \ diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 721ac21a0..00f0e259d 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -2,7 +2,7 @@ # Single JAR contains both frontend and backend with extra fonts for air-gapped environments # Stage 1: Build application with embedded frontend -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build # Install Node.js and npm for frontend build RUN apt-get update && apt-get install -y \ diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index 317265d57..8556d4585 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -2,7 +2,7 @@ # Single JAR contains both frontend and backend with minimal dependencies # Stage 1: Build application with embedded frontend -FROM gradle:8.14-jdk21 AS build +FROM gradle:8.14-jdk21@sha256:051d9a116793bdc5175a3f97a545718b750489eee85a7da20913c8a53f722a72 AS build # Install Node.js and npm for frontend build RUN apt-get update && apt-get install -y \ @@ -36,7 +36,7 @@ RUN DISABLE_ADDITIONAL_FEATURES=true \ ./gradlew clean build -PbuildWithFrontend=true -x spotlessApply -x spotlessCheck -x test -x sonarqube # Stage 2: Runtime image -FROM alpine:3.22.1 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 ARG VERSION_TAG diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index c7d6189e5..a53dec5db 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -1,5 +1,5 @@ # Frontend Dockerfile - React/Vite application -FROM node:20-alpine AS build +FROM node:20-alpine@sha256:658d0f63e501824d6c23e06d4bb95c71e7d704537c9d9272f488ac03a370d448 AS build WORKDIR /app @@ -16,7 +16,7 @@ COPY frontend . RUN npm run build # Production stage -FROM nginx:alpine +FROM nginx:alpine@sha256:8491795299c8e739b7fcc6285d531d9812ce2666e07bd3dd8db00020ad132295 # Copy built files from build stage COPY --from=build /app/dist /usr/share/nginx/html