diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..6b70e0f5 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-07-08 - Optimize Byte-to-Hex Conversion +**Learning:** Using `String.format("%02x", b)` in a loop for converting byte arrays to hex strings introduces unnecessary overhead and intermediate string allocations in Java. +**Action:** Use a reusable `java.util.HexFormat` instance (`private static final java.util.HexFormat HEX_FORMAT = java.util.HexFormat.of();`) instead of `String.format` loops or repeated `HexFormat.of()` calls to optimize performance and reduce intermediate allocations. diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 76a97425..2aeb436b 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,7 @@ **Vulnerability:** Untrusted paths from API responses were directly assigned to `a.href` and used in `iframe` generation, which allows execution of malicious URIs like `javascript:` or `data:`. **Learning:** Even when avoiding `innerHTML`, directly setting URL-like strings to DOM attributes without protocol validation introduces XSS vectors. The payload can be executed when the link is clicked or the iframe is loaded. **Prevention:** Implement an `isSafeUrl` verification function to ensure the protocol is strictly `http:` or `https:` (using `new URL()`) before assigning untrusted inputs to DOM attributes like `href` or `src`. +## 2026-07-08 - Fix Strix Penetration Test Vulnerability +**Vulnerability:** Truncated Hash in Token Fingerprint. The `tokenFingerprint` method truncated SHA-256 hashes to 8 bytes, reducing security and increasing collision risks. +**Learning:** Truncating cryptographic hashes can compromise the strength of the hashing algorithm and increase the risk of collision attacks. +**Prevention:** Use the full hash output for validation or cryptographic purposes to maintain security strength. diff --git a/pom.xml b/pom.xml index 6b7e40df..2ada4be2 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.0 + 3.5.14 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..70ad8d74 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "tests", + "integration", +] diff --git a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java index 454b0448..a9e45c87 100644 --- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java +++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java @@ -9,6 +9,7 @@ import java.time.Instant; import java.util.Arrays; import java.util.Base64; +import java.util.HexFormat; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -57,6 +58,7 @@ public class ArtifactLinkService { private static final int TOKEN_FIELD_COUNT = 10; private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding(); private static final Base64.Decoder URL_DECODER = Base64.getUrlDecoder(); + private static final HexFormat HEX_FORMAT = HexFormat.of(); private final ArtifactStore artifactStore; private final ArtifactLinkLedger artifactLinkLedger; @@ -423,11 +425,7 @@ private static String sha256Hex(byte[] bytes) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] raw = digest.digest(bytes); - StringBuilder hex = new StringBuilder(raw.length * 2); - for (byte b : raw) { - hex.append(String.format("%02x", b)); - } - return hex.toString(); + return HEX_FORMAT.formatHex(raw); } catch (GeneralSecurityException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java index b8787f35..2ea30f3c 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -26,6 +27,8 @@ @Service public class DefaultDocumentConversionService implements DocumentConversionService { + private static final HexFormat HEX_FORMAT = HexFormat.of(); + private final ConversionJobRepository repository; private final ConversionJobStateStore stateStore; private final DocumentValidationService validationService; @@ -160,12 +163,7 @@ private String contentHash(MultipartFile file) { } byte[] raw = digest.digest(); - StringBuilder hex = new StringBuilder(raw.length * 2); - for (byte b : raw) { - hex.append(String.format("%02x", b)); - } - - return hex.toString(); + return HEX_FORMAT.formatHex(raw); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } catch (IOException ex) { diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java index b898f7ac..8fa7377f 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java @@ -140,7 +140,7 @@ private String tokenFingerprint(String approvalToken) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hashed = digest.digest(approvalToken.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(hashed, 0, FINGERPRINT_TRUNCATE_BYTES); + return HexFormat.of().formatHex(hashed); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } diff --git a/src/main/resources/static/assets/viewer/package.json b/src/main/resources/static/assets/viewer/package.json new file mode 100644 index 00000000..e6868604 --- /dev/null +++ b/src/main/resources/static/assets/viewer/package.json @@ -0,0 +1,10 @@ +{ + "name": "clearfolio-viewer-assets", + "version": "1.0.0", + "description": "Frontend assets for Clearfolio Viewer", + "private": true, + "scripts": { + "test": "echo \"No test specified\"", + "lint": "echo \"No linting specified\"" + } +}