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.
This commit is contained in:
Ludy
2026-01-13 14:59:59 +01:00
committed by GitHub
parent 251ad63ea6
commit 3c8fb8ac96
12 changed files with 27 additions and 22 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 .

View File

@@ -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 .

View File

@@ -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 .

View File

@@ -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 \

View File

@@ -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 \

View File

@@ -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

View File

@@ -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