diff --git a/.github/workflows/file_hash_generation.yml b/.github/workflows/file_hash_generation.yml new file mode 100644 index 00000000..a0e33223 --- /dev/null +++ b/.github/workflows/file_hash_generation.yml @@ -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 \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/TemplateIntegrityConfig.java b/src/main/java/stirling/software/SPDF/config/TemplateIntegrityConfig.java new file mode 100644 index 00000000..d5cc93b6 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/TemplateIntegrityConfig.java @@ -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 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 loadReferenceHashes() throws IOException { + Resource resource = resourceLoader.getResource(referenceHashPath); + try (InputStream is = resource.getInputStream()) { + String content = new String(is.readAllBytes()); + return parseHashJson(content); + } + } + + private Map parseHashJson(String json) { + Map 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 referenceHashes) throws IOException { + // Track files we've found to check for missing files later + Map 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 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()); + } +} \ No newline at end of file diff --git a/src/main/resources/reference-hash.json b/src/main/resources/reference-hash.json new file mode 100644 index 00000000..e69de29b diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index fa83e2ff..7f275540 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -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 diff --git a/testing/webpage_urls.txt b/testing/webpage_urls.txt index 8ccaaf0b..973a8737 100644 --- a/testing/webpage_urls.txt +++ b/testing/webpage_urls.txt @@ -51,3 +51,4 @@ /swagger-ui/index.html /licenses /releases +/v1/api-docs \ No newline at end of file diff --git a/testing/webpage_urls_full.txt b/testing/webpage_urls_full.txt index 86b90872..1ea61d8f 100644 --- a/testing/webpage_urls_full.txt +++ b/testing/webpage_urls_full.txt @@ -62,4 +62,5 @@ /stamp /validate-signature /view-pdf -/swagger-ui/index.html \ No newline at end of file +/swagger-ui/index.html +/v1/api-docs \ No newline at end of file