This commit is contained in:
Anthony Stirling 2025-04-16 12:05:47 +01:00
parent 6906344178
commit 13c7a1fb16
6 changed files with 278 additions and 19 deletions

View File

@ -0,0 +1,81 @@
name: Generate Template Hashes
on:
push:
branches:
- main
- hashs
paths:
- 'src/main/resources/templates/**'
- 'src/main/resources/static/**'
workflow_dispatch: # Allow manual triggering
jobs:
generate-hash:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Calculate template hashes
id: hash
run: |
# Create a script to calculate individual file hashes
cat > calculate-hashes.sh << 'EOF'
#!/bin/bash
set -e
# Directories to hash
TEMPLATE_DIR="src/main/resources/templates"
STATIC_DIR="src/main/resources/static"
OUTPUT_FILE="src/main/resources/reference-hash.json"
# Create output directory if it doesn't exist
mkdir -p $(dirname "$OUTPUT_FILE")
# Start JSON
echo "{" > "$OUTPUT_FILE"
# Find all files and calculate CRC32 hash
FIRST=true
find "$TEMPLATE_DIR" "$STATIC_DIR" -type f | sort | while read file; do
# Get relative path from src/main/resources
REL_PATH=$(echo "$file" | sed 's|^src/main/resources/||')
# Calculate CRC32 hash (faster than MD5)
HASH=$(crc32 "$file" 2>/dev/null || cksum "$file" | awk '{print $1}')
# Add to JSON
if [ "$FIRST" = true ]; then
FIRST=false
else
echo "," >> "$OUTPUT_FILE"
fi
echo " \"$REL_PATH\": \"$HASH\"" >> "$OUTPUT_FILE"
done
# End JSON
echo "}" >> "$OUTPUT_FILE"
echo "Generated hashes for $(grep -c ":" "$OUTPUT_FILE") files"
EOF
chmod +x calculate-hashes.sh
./calculate-hashes.sh
- name: Commit and push if changed
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "GitHub Actions"
git add src/main/resources/reference-hash.json
# Only commit if there are changes
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Update template reference hashes [skip ci]"
git push
fi

View File

@ -0,0 +1,172 @@
package stirling.software.SPDF.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
@Configuration
public class TemplateIntegrityConfig {
private static final Logger logger = LoggerFactory.getLogger(TemplateIntegrityConfig.class);
// Buffer size for reading files (8KB is a good balance)
private static final int BUFFER_SIZE = 8192;
private final ResourceLoader resourceLoader;
@Value("${template.hash.reference:classpath:reference-hash.json}")
private String referenceHashPath;
@Value("${template.directories:classpath:templates/,classpath:static/}")
private String[] templateDirectories;
public TemplateIntegrityConfig(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean
public boolean templatesModified() {
try {
Map<String, String> referenceHashes = loadReferenceHashes();
// Check for modifications with early termination
if (checkForModifications(referenceHashes)) {
logger.info("SECURITY WARNING: Templates appear to have been modified from the release version!");
return true;
}
logger.info("Template integrity verified successfully");
return false;
} catch (Exception e) {
logger.error("Error verifying template integrity", e);
// In case of error, assume modified for security
return true;
}
}
private Map<String, String> loadReferenceHashes() throws IOException {
Resource resource = resourceLoader.getResource(referenceHashPath);
try (InputStream is = resource.getInputStream()) {
String content = new String(is.readAllBytes());
return parseHashJson(content);
}
}
private Map<String, String> parseHashJson(String json) {
Map<String, String> result = new HashMap<>();
// Simple JSON parsing to avoid additional dependencies
String[] entries = json.replaceAll("[{}\"]", "").split(",");
for (String entry : entries) {
String[] parts = entry.trim().split(":");
if (parts.length == 2) {
result.put(parts[0].trim(), parts[1].trim());
}
}
return result;
}
private boolean checkForModifications(Map<String, String> referenceHashes) throws IOException {
// Track files we've found to check for missing files later
Map<String, Boolean> foundFiles = new HashMap<>();
for (String key : referenceHashes.keySet()) {
foundFiles.put(key, false);
}
AtomicBoolean modified = new AtomicBoolean(false);
// Check each directory
for (String dir : templateDirectories) {
if (modified.get()) {
break; // Early termination
}
// Remove classpath: prefix if present
String dirPath = dir.replace("classpath:", "");
// Get the resource as a file
Resource resource = resourceLoader.getResource("classpath:" + dirPath);
try {
Path directory = Paths.get(resource.getURI());
if (Files.exists(directory) && Files.isDirectory(directory)) {
// Walk the directory tree
Files.walk(directory)
.filter(Files::isRegularFile)
.forEach(path -> {
if (modified.get()) return; // Skip if already found modification
try {
String relativePath = directory.relativize(path).toString();
// Track that we found this file
foundFiles.put(relativePath, true);
// Check if this file is in our reference
String referenceHash = referenceHashes.get(relativePath);
if (referenceHash == null) {
// New file found
logger.info("New file detected: {}", relativePath);
modified.set(true);
return;
}
// Check if the hash matches
String currentHash = computeFileHash(path);
if (!currentHash.equals(referenceHash)) {
logger.info("Modified file detected: {}", relativePath);
modified.set(true);
}
} catch (IOException e) {
logger.info("Failed to hash file: {}", path, e);
modified.set(true); // Fail safe
}
});
}
} catch (Exception e) {
logger.error("Error accessing directory: {}", dirPath, e);
return true; // Assume modified on error
}
}
// If we haven't found a modification yet, check for missing files
if (!modified.get()) {
for (Map.Entry<String, Boolean> entry : foundFiles.entrySet()) {
if (!entry.getValue()) {
// File was in reference but not found
logger.info("Missing file detected: {}", entry.getKey());
return true;
}
}
}
return modified.get();
}
private String computeFileHash(Path filePath) throws IOException {
Checksum checksum = new CRC32(); // Much faster than MD5 or SHA
try (InputStream is = Files.newInputStream(filePath)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
checksum.update(buffer, 0, bytesRead);
}
}
return Long.toHexString(checksum.getValue());
}
}

View File

View File

@ -6,12 +6,10 @@
# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| #
# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| #
# #
# Custom setting.yml file with all endpoints disabled to only be used for testing purposes #
# Do not comment out any entry, it will be removed on next startup #
# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME #
#############################################################################################################
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
@ -62,15 +60,21 @@ security:
privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair
spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
premium:
key: 00000000-0000-0000-0000-000000000000
SSOAutoLogin: false # Enable to auto login to first provided SSO
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
enabled: false # Enable license key checks for pro/enterprise features
proFeatures:
SSOAutoLogin: false
CustomMetadata:
autoUpdateMetadata: false
author: username
creator: Stirling-PDF
producer: Stirling-PDF
googleDrive:
enabled: false
clientId: ''
apiKey: ''
appId: ''
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
@ -88,6 +92,7 @@ system:
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: true # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
datasource:
enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration
@ -100,13 +105,12 @@ system:
name: postgres # set the name of your database. Should match the name of the database you create
customPaths:
pipeline:
watchedFoldersDir: "" #Defaults to /pipeline/watchedFolders
finishedFoldersDir: "" #Defaults to /pipeline/finishedFolders
watchedFoldersDir: '' #Defaults to /pipeline/watchedFolders
finishedFoldersDir: '' #Defaults to /pipeline/finishedFolders
operations:
weasyprint: "" #Defaults to /opt/venv/bin/weasyprint
unoconvert: "" #Defaults to /opt/venv/bin/unoconvert
weasyprint: '' #Defaults to /opt/venv/bin/weasyprint
unoconvert: '' #Defaults to /opt/venv/bin/unoconvert
fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB".
ui:
appName: '' # application's visible name
@ -114,7 +118,7 @@ ui:
appNameNavbar: '' # name displayed on the navigation bar
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
endpoints: # All the possible endpoints are disabled
endpoints:
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
@ -125,7 +129,7 @@ metrics:
AutomaticallyGenerated:
key: cbb81c0f-50b1-450c-a2b5-89ae527776eb
UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a
appVersion: 0.44.3
appVersion: 0.45.6
processExecutor:
sessionLimit: # Process executor instances limits

View File

@ -51,3 +51,4 @@
/swagger-ui/index.html
/licenses
/releases
/v1/api-docs

View File

@ -62,4 +62,5 @@
/stamp
/validate-signature
/view-pdf
/swagger-ui/index.html
/swagger-ui/index.html
/v1/api-docs