You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2026/08/31 11:37:27 UTC

(maven-resolver) branch master updated: fix: bound response reads and sanitize remote-derived strings (#2081)

This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/master by this push:
     new 7006fc33c fix: bound response reads and sanitize remote-derived strings (#2081)
7006fc33c is described below

commit 7006fc33cfb2c48e01718edbede04aff9f2bbc0a
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 13:37:21 2026 +0200

    fix: bound response reads and sanitize remote-derived strings (#2081)
    
    ## Summary
    
    Fixes 4 findings from the maven-resolver security audit (scan-maven-resolver-20260811):
    
    | Finding | Severity | Description |
    |---------|----------|-------------|
    | f004 | MEDIUM | HTTP error-response bodies read unbounded into memory (decompression-amplified OOM) |
    | f024 | LOW | Hostile remote checksum file causes unbounded allocation |
    | f026 | LOW | Server Last-Modified future date permanently pins artifact against refresh |
    | f028 | LOW | Remote-derived strings reach console logs without control-character sanitization |
    
    **Root cause:** Response bodies, checksum files, timestamps, and strings from the remote side are consumed with no size cap, no plausibility clamp, and no control-character sanitization.
    
    **Fix:** Bound before allocating: cap error-body and checksum reads, clamp timestamps, sanitize control characters in log messages.
---
 .../aether/transfer/ChecksumFailureException.java  |  38 ++++++-
 .../transfer/ChecksumFailureExceptionTest.java     |  73 +++++++++++++
 .../internal/impl/AbstractChecksumPolicy.java      |   6 +-
 .../internal/impl/DefaultArtifactResolver.java     |   9 +-
 .../internal/impl/DefaultChecksumProcessor.java    |  72 ++++++++++---
 .../aether/internal/impl/DefaultFileProcessor.java |  17 +--
 .../internal/impl/DefaultUpdateCheckManager.java   |   8 +-
 .../eclipse/aether/internal/impl/LogSanitizer.java |  58 ++++++++++
 .../aether/internal/impl/WarnChecksumPolicy.java   |   8 +-
 .../impl/DefaultChecksumProcessorTest.java         | 111 +++++++++++++++++++
 .../aether/internal/impl/LogSanitizerTest.java     |  80 ++++++++++++++
 .../transport/http/RFC9457/RFC9457Reporter.java    |  50 ++++++++-
 .../http/RFC9457/RFC9457ReporterTest.java          | 118 +++++++++++++++++++++
 .../transport/apache/ApacheRFC9457Reporter.java    |  15 ++-
 .../aether/transport/apache/ApacheTransporter.java |   4 +-
 .../aether/transport/jdk/JdkRFC9457Reporter.java   |   3 +-
 .../aether/transport/jdk/JdkTransporter.java       |   7 +-
 .../transport/jetty/JettyRFC9457Reporter.java      |   3 +-
 .../aether/transport/jetty/JettyTransporter.java   |   3 +-
 .../aether/transport/url/UrlTransporter.java       |   3 +-
 .../org/eclipse/aether/util/ChecksumUtils.java     |  68 +++++++++---
 .../transport/http/HttpTransporterUtils.java       |  19 ++++
 .../transport/http/HttpTransporterUtilsTest.java   |  20 ++++
 23 files changed, 720 insertions(+), 73 deletions(-)

diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java
index 100bb342b..c80ca67e5 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ChecksumFailureException.java
@@ -68,14 +68,42 @@ public class ChecksumFailureException extends RepositoryException {
     public static ChecksumFailureException mismatchDetail(
             String detail, String expected, String expectedKind, String actual) {
         String message = "Checksum validation failed, expected '"
-                + expected + "'" + (expectedKind == null ? "" : " (" + expectedKind + ")")
-                + " but is actually '" + actual + "'";
+                + sanitize(expected) + "'" + (expectedKind == null ? "" : " (" + sanitize(expectedKind) + ")")
+                + " but is actually '" + sanitize(actual) + "'";
         if (detail != null) {
-            message = message + " (" + detail + ")";
+            message = message + " (" + sanitize(detail) + ")";
         }
         return new ChecksumFailureException(message, null, expected, expectedKind, actual, true);
     }
 
+    /**
+     * Replaces ISO control characters below U+0020 (except LF and TAB) and DEL (U+007F) with their visible
+     * {@code \}{@code uXXXX} escapes. The expected checksum value is remote-supplied and ends up in log output (the
+     * default "warn" checksum policy logs this exception's message as the sole integrity warning): raw terminal
+     * control characters such as ESC or CR embedded in it could erase or forge that log line. The
+     * {@link #getExpected()}, {@link #getExpectedKind()} and {@link #getActual()} accessors keep returning the
+     * raw values.
+     */
+    private static String sanitize(String value) {
+        if (value == null) {
+            return null;
+        }
+        StringBuilder result = null;
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            if ((c < 0x20 && c != '\n' && c != '\t') || c == 0x7f) {
+                if (result == null) {
+                    result = new StringBuilder(value.length() + 16);
+                    result.append(value, 0, i);
+                }
+                result.append(String.format("\\u%04X", (int) c));
+            } else if (result != null) {
+                result.append(c);
+            }
+        }
+        return result == null ? value : result.toString();
+    }
+
     /**
      * Use in case of checksum not available. Optionally, one can specify which kind was not available.
      *
@@ -112,8 +140,8 @@ public class ChecksumFailureException extends RepositoryException {
     @Deprecated
     public ChecksumFailureException(String expected, String expectedKind, String actual) {
         super("Checksum validation failed, expected '"
-                + expected + "'" + (expectedKind == null ? "" : " (" + expectedKind + ")")
-                + " but is actually '" + actual + "'");
+                + sanitize(expected) + "'" + (expectedKind == null ? "" : " (" + sanitize(expectedKind) + ")")
+                + " but is actually '" + sanitize(actual) + "'");
         this.expected = expected;
         this.expectedKind = expectedKind;
         this.actual = actual;
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/ChecksumFailureExceptionTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/ChecksumFailureExceptionTest.java
new file mode 100644
index 000000000..b97d27243
--- /dev/null
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/ChecksumFailureExceptionTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.transfer;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ChecksumFailureExceptionTest {
+
+    private static final String ESC = "\u001B";
+
+    private static final String DEL = "\u007F";
+
+    @Test
+    void mismatchMessageCarriesValues() {
+        ChecksumFailureException e = ChecksumFailureException.mismatch("cafebabe", "PROVIDED", "deadbeef");
+        assertTrue(e.getMessage().contains("cafebabe"));
+        assertTrue(e.getMessage().contains("PROVIDED"));
+        assertTrue(e.getMessage().contains("deadbeef"));
+        assertEquals("cafebabe", e.getExpected());
+        assertEquals("PROVIDED", e.getExpectedKind());
+        assertEquals("deadbeef", e.getActual());
+        assertTrue(e.isRetryWorthy());
+    }
+
+    @Test
+    void mismatchMessageEscapesControlCharacters() {
+        // remote-supplied checksum values reach WARN logs via this message; terminal escapes must be neutralized
+        String hostileExpected = "abc" + ESC + "[2K\rdef";
+        String hostileActual = "actual" + DEL;
+        ChecksumFailureException e = ChecksumFailureException.mismatchDetail(
+                "de" + ESC + "tail", hostileExpected, "PROVIDED", hostileActual);
+        String message = e.getMessage();
+        assertFalse(message.contains(ESC), "raw ESC must not survive into the message");
+        assertFalse(message.contains("\r"), "raw CR must not survive into the message");
+        assertFalse(message.contains(DEL), "raw DEL must not survive into the message");
+        assertTrue(message.contains("abc\\u001B[2K\\u000Ddef"));
+        assertTrue(message.contains("actual\\u007F"));
+        assertTrue(message.contains("de\\u001Btail"));
+        // the accessors keep returning the raw values for programmatic consumers
+        assertEquals(hostileExpected, e.getExpected());
+        assertEquals(hostileActual, e.getActual());
+        assertTrue(e.isRetryWorthy());
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    void deprecatedMismatchConstructorEscapesControlCharacters() {
+        ChecksumFailureException e = new ChecksumFailureException("bad" + ESC + "[1A", "PROVIDED", "actual");
+        assertFalse(e.getMessage().contains(ESC));
+        assertTrue(e.getMessage().contains("bad\\u001B[1A"));
+        assertEquals("bad" + ESC + "[1A", e.getExpected());
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java
index 2007042fc..8dd408774 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/AbstractChecksumPolicy.java
@@ -55,7 +55,11 @@ abstract class AbstractChecksumPolicy implements ChecksumPolicy {
             throws ChecksumFailureException {
         requireNonNull(algorithm, "algorithm cannot be null");
         requireNonNull(exception, "exception cannot be null");
-        logger.debug("Could not validate {} checksum for {}", algorithm, resource.getResourceName(), exception);
+        logger.debug(
+                "Could not validate {} checksum for {}",
+                algorithm,
+                LogSanitizer.sanitize(resource.getResourceName()),
+                exception);
     }
 
     @Override
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
index e4ddcde55..dc781a03b 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
@@ -379,11 +379,14 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                     if (local.getPath() != null) {
                         LOGGER.info(
                                 "Artifact {} is present in the local repository, but cached from a remote repository ID that is unavailable in current build context, verifying that is downloadable from {}",
-                                artifact,
-                                remoteRepositories);
+                                LogSanitizer.sanitize(String.valueOf(artifact)),
+                                LogSanitizer.sanitize(String.valueOf(remoteRepositories)));
                     }
 
-                    LOGGER.debug("Resolving artifact {} from {}", artifact, remoteRepositories);
+                    LOGGER.debug(
+                            "Resolving artifact {} from {}",
+                            LogSanitizer.sanitize(String.valueOf(artifact)),
+                            LogSanitizer.sanitize(String.valueOf(remoteRepositories)));
                     AtomicBoolean resolved = new AtomicBoolean(false);
                     Iterator<ResolutionGroup> groupIt = groups.iterator();
                     for (RemoteRepository repo : filteredRemoteRepositories) {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java
index 5def5234c..e558b51fc 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java
@@ -39,6 +39,15 @@ import static java.util.Objects.requireNonNull;
 @Singleton
 @Named
 public class DefaultChecksumProcessor implements ChecksumProcessor {
+    /**
+     * Upper bound (in characters) for data read from a checksum file. Every sane checksum file format fits well
+     * within this limit (the longest is "SHA-512 (&lt;file name&gt;) = &lt;128 hex chars&gt;"). Checksum files
+     * are fetched from remote repositories: without a bound, a hostile repository answering a checksum request
+     * with a multi-gigabyte single-line body would be buffered wholesale into memory, exhausting the build JVM
+     * heap. Longer input is rejected as malformed instead of being buffered.
+     */
+    static final int MAX_CHECKSUM_FILE_CHARS = 8192;
+
     private final PathProcessor pathProcessor;
 
     @Inject
@@ -48,23 +57,12 @@ public class DefaultChecksumProcessor implements ChecksumProcessor {
 
     @Override
     public String readChecksum(final Path checksumPath) throws IOException {
-        // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
-        String checksum = "";
+        String checksum;
         try (BufferedReader br = Files.newBufferedReader(checksumPath, StandardCharsets.UTF_8)) {
-            while (true) {
-                String line = br.readLine();
-                if (line == null) {
-                    break;
-                }
-                line = line.trim();
-                if (!line.isEmpty()) {
-                    checksum = line;
-                    break;
-                }
-            }
+            checksum = readFirstNonEmptyLine(br, checksumPath.toString());
         }
 
-        if (checksum.matches(".+= [0-9A-Fa-f]+")) {
+        if (isAlgorithmHeaderFormat(checksum)) {
             int lastSpacePos = checksum.lastIndexOf(' ');
             checksum = checksum.substring(lastSpacePos + 1);
         } else {
@@ -78,6 +76,52 @@ public class DefaultChecksumProcessor implements ChecksumProcessor {
         return checksum;
     }
 
+    /**
+     * Reads the first non-empty line, enforcing {@link #MAX_CHECKSUM_FILE_CHARS} on the total amount of data
+     * consumed. Returns the trimmed line, or an empty string if the stream holds no non-empty line.
+     */
+    static String readFirstNonEmptyLine(BufferedReader reader, String source) throws IOException {
+        StringBuilder buffer = new StringBuilder(64);
+        int read = 0;
+        int c;
+        while ((c = reader.read()) != -1) {
+            if (++read > MAX_CHECKSUM_FILE_CHARS) {
+                throw new IOException("Checksum file " + source + " is malformed: longer than "
+                        + MAX_CHECKSUM_FILE_CHARS + " characters");
+            }
+            if (c == '\n' || c == '\r') {
+                String line = buffer.toString().trim();
+                if (!line.isEmpty()) {
+                    return line;
+                }
+                buffer.setLength(0);
+            } else {
+                buffer.append((char) c);
+            }
+        }
+        return buffer.toString().trim();
+    }
+
+    /**
+     * Non-backtracking equivalent of {@code line.matches(".+= [0-9A-Fa-f]+")}: at least one character, followed
+     * by "= ", followed by one or more hex digits reaching the end of the line ("&lt;algorithm&gt; (&lt;file&gt;)
+     * = &lt;hex&gt;" style checksum lines).
+     */
+    static boolean isAlgorithmHeaderFormat(String line) {
+        int lastSpacePos = line.lastIndexOf(' ');
+        if (lastSpacePos < 2 || lastSpacePos == line.length() - 1 || line.charAt(lastSpacePos - 1) != '=') {
+            return false;
+        }
+        for (int i = lastSpacePos + 1; i < line.length(); i++) {
+            char ch = line.charAt(i);
+            boolean hex = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
+            if (!hex) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     @Override
     public void writeChecksum(Path target, String checksum) throws IOException {
         // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
index ed4660ac7..949ddb0b4 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
@@ -145,24 +145,13 @@ public class DefaultFileProcessor implements FileProcessor {
 
     @Override
     public String readChecksum(final File checksumFile) throws IOException {
-        // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
-        String checksum = "";
+        String checksum;
         try (BufferedReader br = new BufferedReader(
                 new InputStreamReader(Files.newInputStream(checksumFile.toPath()), StandardCharsets.UTF_8), 512)) {
-            while (true) {
-                String line = br.readLine();
-                if (line == null) {
-                    break;
-                }
-                line = line.trim();
-                if (!line.isEmpty()) {
-                    checksum = line;
-                    break;
-                }
-            }
+            checksum = DefaultChecksumProcessor.readFirstNonEmptyLine(br, checksumFile.toString());
         }
 
-        if (checksum.matches(".+= [0-9A-Fa-f]+")) {
+        if (DefaultChecksumProcessor.isAlgorithmHeaderFormat(checksum)) {
             int lastSpacePos = checksum.lastIndexOf(' ');
             checksum = checksum.substring(lastSpacePos + 1);
         } else {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
index 0a8af9304..9eab98a89 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
@@ -235,7 +235,7 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
                             + repository.getUrl() + " during a previous attempt. This failure"
                             + " was cached in the local repository and"
                             + " resolution is not reattempted until the update interval of " + repository.getId()
-                            + " has elapsed or updates are forced. Original error: " + error,
+                            + " has elapsed or updates are forced. Original error: " + LogSanitizer.sanitize(error),
                     true);
         }
     }
@@ -333,7 +333,7 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
                             + repository.getUrl() + " during a previous attempt."
                             + " This failure was cached in the local repository and"
                             + " resolution will not be reattempted until the update interval of " + repository.getId()
-                            + " has elapsed or updates are forced. Original error: " + error,
+                            + " has elapsed or updates are forced. Original error: " + LogSanitizer.sanitize(error),
                     true);
         }
     }
@@ -563,7 +563,9 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
             if (msg == null || msg.isEmpty()) {
                 msg = error.getClass().getSimpleName();
             }
-            updates.put(dataKey + ERROR_KEY_SUFFIX, msg);
+            // transfer error messages may embed remote-derived bytes: neutralize terminal control characters
+            // before persisting them, as they are re-read and logged on subsequent update checks
+            updates.put(dataKey + ERROR_KEY_SUFFIX, LogSanitizer.sanitize(msg));
             updates.put(dataKey + UPDATED_KEY_SUFFIX, null);
             updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp);
         }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LogSanitizer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LogSanitizer.java
new file mode 100644
index 000000000..e27e2f2f3
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LogSanitizer.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+/**
+ * Sanitizes strings that may carry remote-derived bytes (artifact coordinates, checksum values, transfer error
+ * messages) before they are logged or persisted. Bytes received from a remote repository must not be able to
+ * influence terminal rendering: raw control characters such as ESC (ANSI escape sequences) or CR (line rewrite)
+ * embedded in, for example, a transitive POM's coordinates could otherwise erase or forge log lines - including
+ * the sole integrity WARNING emitted under the default "warn" checksum policy.
+ *
+ * @since 2.0.23
+ */
+final class LogSanitizer {
+    private LogSanitizer() {}
+
+    /**
+     * Replaces every ISO control character below U+0020 (except LF and TAB) as well as DEL (U+007F) with its
+     * visible {@code \}{@code uXXXX} escape, preserving the evidence while neutralizing terminal escape
+     * sequences. Returns the input instance unchanged (no allocation) when nothing needs escaping; returns
+     * {@code null} for {@code null} input.
+     */
+    static String sanitize(String value) {
+        if (value == null) {
+            return null;
+        }
+        StringBuilder result = null;
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            if ((c < 0x20 && c != '\n' && c != '\t') || c == 0x7f) {
+                if (result == null) {
+                    result = new StringBuilder(value.length() + 16);
+                    result.append(value, 0, i);
+                }
+                result.append(String.format("\\u%04X", (int) c));
+            } else if (result != null) {
+                result.append(c);
+            }
+        }
+        return result == null ? value : result.toString();
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java
index f9c47d7d3..2d582025e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/WarnChecksumPolicy.java
@@ -34,8 +34,8 @@ final class WarnChecksumPolicy extends AbstractChecksumPolicy {
     public boolean onTransferChecksumFailure(ChecksumFailureException exception) {
         logger.warn(
                 "Could not validate integrity of download from {}{}",
-                resource.getRepositoryUrl(),
-                resource.getResourceName(),
+                LogSanitizer.sanitize(resource.getRepositoryUrl()),
+                LogSanitizer.sanitize(resource.getResourceName()),
                 exception);
         return true;
     }
@@ -44,7 +44,7 @@ final class WarnChecksumPolicy extends AbstractChecksumPolicy {
     public void onNoMoreChecksums() {
         logger.warn(
                 "Could not validate integrity of download from {}{}: no checksums available",
-                resource.getRepositoryUrl(),
-                resource.getResourceName());
+                LogSanitizer.sanitize(resource.getRepositoryUrl()),
+                LogSanitizer.sanitize(resource.getResourceName()));
     }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessorTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessorTest.java
new file mode 100644
index 000000000..7ac4696fd
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessorTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DefaultChecksumProcessorTest {
+
+    private static final String SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
+
+    @TempDir
+    private Path targetDir;
+
+    private DefaultChecksumProcessor checksumProcessor;
+
+    @BeforeEach
+    void setup() {
+        checksumProcessor = new DefaultChecksumProcessor(new DefaultPathProcessor());
+    }
+
+    private Path write(String content) throws IOException {
+        Path file = targetDir.resolve("checksum.sha1");
+        Files.write(file, content.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
+    @Test
+    void testReadChecksum() throws IOException {
+        assertEquals(SHA1, checksumProcessor.readChecksum(write(SHA1)));
+    }
+
+    @Test
+    void testReadChecksumEmptyFile() throws IOException {
+        assertEquals("", checksumProcessor.readChecksum(write("")));
+    }
+
+    @Test
+    void testReadChecksumLeadingEmptyLinesAndWhitespace() throws IOException {
+        assertEquals(SHA1, checksumProcessor.readChecksum(write("\n\r\n  \n " + SHA1 + " \n")));
+    }
+
+    @Test
+    void testReadChecksumAlgorithmHeaderFormat() throws IOException {
+        assertEquals(SHA1, checksumProcessor.readChecksum(write("foobar(alg) = " + SHA1)));
+    }
+
+    @Test
+    void testReadChecksumFirstTokenFormat() throws IOException {
+        assertEquals(SHA1, checksumProcessor.readChecksum(write(SHA1 + " foobar")));
+    }
+
+    @Test
+    void testReadChecksumNonHexTailFallsBackToFirstToken() throws IOException {
+        // "= " present but tail is not hex: not the "<algorithm> (<file>) = <hex>" format,
+        // so the first token wins (matches the historic regex-based behavior)
+        assertEquals("foo=", checksumProcessor.readChecksum(write("foo= xyz")));
+    }
+
+    @Test
+    void testReadChecksumOversizedInputRejected() throws IOException {
+        char[] big = new char[DefaultChecksumProcessor.MAX_CHECKSUM_FILE_CHARS + 1];
+        Arrays.fill(big, 'a');
+        Path file = write(new String(big));
+        IOException e = assertThrows(IOException.class, () -> checksumProcessor.readChecksum(file));
+        assertTrue(e.getMessage().contains("malformed"));
+    }
+
+    @Test
+    void testReadChecksumOversizedSecondLineRejected() throws IOException {
+        // the bound is on total data consumed, not per line: a checksum hidden behind a huge
+        // preamble of blank lines is equally malformed
+        char[] blanks = new char[DefaultChecksumProcessor.MAX_CHECKSUM_FILE_CHARS + 1];
+        Arrays.fill(blanks, '\n');
+        Path file = write(new String(blanks) + SHA1);
+        assertThrows(IOException.class, () -> checksumProcessor.readChecksum(file));
+    }
+
+    @Test
+    void testReadChecksumWithinBoundAccepted() throws IOException {
+        // a legal checksum preceded by a modest preamble stays accepted
+        assertEquals(SHA1, checksumProcessor.readChecksum(write("\n\n\n" + SHA1)));
+    }
+}
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/LogSanitizerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/LogSanitizerTest.java
new file mode 100644
index 000000000..8dad31f45
--- /dev/null
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/LogSanitizerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class LogSanitizerTest {
+
+    private static final char ESC = '\u001B';
+
+    private static final char DEL = '\u007F';
+
+    @Test
+    void nullPassesThrough() {
+        assertNull(LogSanitizer.sanitize(null));
+    }
+
+    @Test
+    void cleanStringReturnsSameInstance() {
+        String clean = "org.apache.maven.resolver:artifact:jar:1.0 from https://repo.example.com/";
+        assertSame(clean, LogSanitizer.sanitize(clean));
+    }
+
+    @Test
+    void newlineAndTabArePreserved() {
+        String value = "line1\nline2\tcolumn";
+        assertSame(value, LogSanitizer.sanitize(value));
+    }
+
+    @Test
+    void escapeSequenceIsNeutralized() {
+        // ESC[2K erases the current terminal line, CR returns the cursor: together they cancel a WARNING line
+        assertEquals("abc\\u001B[2K\\u000Ddef", LogSanitizer.sanitize("abc" + ESC + "[2K\rdef"));
+    }
+
+    @Test
+    void carriageReturnIsEscaped() {
+        assertEquals("evil\\u000D[INFO] BUILD SUCCESS", LogSanitizer.sanitize("evil\r[INFO] BUILD SUCCESS"));
+    }
+
+    @Test
+    void deleteAndLowControlsAreEscaped() {
+        assertEquals("a\\u007Fb\\u0000c\\u0008d", LogSanitizer.sanitize("a" + DEL + "b\u0000c\bd"));
+    }
+
+    @Test
+    void sanitizedOutputContainsNoRawControls() {
+        StringBuilder hostile = new StringBuilder("x");
+        for (char c = 0; c < 0x20; c++) {
+            hostile.append(c);
+        }
+        hostile.append(DEL).append('y');
+        String sanitized = LogSanitizer.sanitize(hostile.toString());
+        for (int i = 0; i < sanitized.length(); i++) {
+            char c = sanitized.charAt(i);
+            assertFalse((c < 0x20 && c != '\n' && c != '\t') || c == DEL, "raw control char at index " + i);
+        }
+    }
+}
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java
index 7fb51a945..a250ffebf 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457Reporter.java
@@ -18,7 +18,10 @@
  */
 package org.eclipse.aether.spi.connector.transport.http.RFC9457;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 
 /**
  * A reporter for RFC 9457 messages.
@@ -38,6 +41,14 @@ public abstract class RFC9457Reporter<T, E extends Exception, R> {
     public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON = "application/problem+json";
     public static final String CONTENT_TYPE_PROBLEM_DETAILS_JSON_AND_ANY = "application/problem+json,*/*";
 
+    /**
+     * Maximum number of bytes read from an error response body. Legitimate RFC 9457 payloads are
+     * tiny, while the body of an error response is attacker influenced data (the content type header
+     * alone triggers consumption) and may be transparently decompressed by the transport, so it must
+     * never be buffered unbounded. Bodies larger than this limit are truncated, not rejected.
+     */
+    public static final int MAX_BODY_BYTES = 64 * 1024;
+
     protected abstract boolean isRFC9457Message(T response);
 
     protected abstract int getStatusCode(T response);
@@ -54,6 +65,30 @@ public abstract class RFC9457Reporter<T, E extends Exception, R> {
      */
     public abstract void prepareRequest(R request);
 
+    /**
+     * Reads the given stream into a UTF-8 string, consuming at most {@link #MAX_BODY_BYTES} bytes.
+     * Any remaining bytes are left unread, so an oversized (or decompression amplified) body is
+     * truncated instead of exhausting memory. The caller remains responsible for closing the stream.
+     *
+     * @param is The stream to read the body from, must not be {@code null}.
+     * @return The body as UTF-8 string, truncated to {@link #MAX_BODY_BYTES} bytes, never {@code null}.
+     * @throws IOException If reading the stream fails.
+     */
+    protected static String readBody(InputStream is) throws IOException {
+        ByteArrayOutputStream body = new ByteArrayOutputStream();
+        byte[] chunk = new byte[8 * 1024];
+        int remaining = MAX_BODY_BYTES;
+        while (remaining > 0) {
+            int read = is.read(chunk, 0, Math.min(chunk.length, remaining));
+            if (read < 0) {
+                break;
+            }
+            body.write(chunk, 0, read);
+            remaining -= read;
+        }
+        return body.toString(StandardCharsets.UTF_8.name());
+    }
+
     protected boolean hasRFC9457ContentType(String contentType) {
         if (contentType == null) {
             return false;
@@ -88,8 +123,19 @@ public abstract class RFC9457Reporter<T, E extends Exception, R> {
             }
 
             if (body != null && !body.isEmpty()) {
-                RFC9457Payload rfc9457Payload = RFC9457Parser.parse(body);
-                throw new HttpRFC9457Exception(statusCode, reasonPhrase, rfc9457Payload);
+                RFC9457Payload rfc9457Payload;
+                try {
+                    rfc9457Payload = RFC9457Parser.parse(body);
+                } catch (RuntimeException ignore) {
+                    // Malformed (possibly truncated, see MAX_BODY_BYTES) problem details
+                    // must not change the error classification.
+                    rfc9457Payload = null;
+                }
+                if (rfc9457Payload != null) {
+                    throw new HttpRFC9457Exception(statusCode, reasonPhrase, rfc9457Payload);
+                }
+                baseException.accept(statusCode, reasonPhrase);
+                return;
             }
             throw new HttpRFC9457Exception(statusCode, reasonPhrase, RFC9457Payload.INSTANCE);
         }
diff --git a/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java
index 95db2325a..6e1cad9f6 100644
--- a/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java
+++ b/maven-resolver-spi/src/test/java/org/eclipse/aether/spi/connector/transport/http/RFC9457/RFC9457ReporterTest.java
@@ -18,12 +18,58 @@
  */
 package org.eclipse.aether.spi.connector.transport.http.RFC9457;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 class RFC9457ReporterTest {
 
+    private static final class TestReporter extends RFC9457Reporter<String, IOException, Object> {
+        private final boolean rfc9457;
+
+        private boolean bodyRead;
+
+        private TestReporter(boolean rfc9457) {
+            this.rfc9457 = rfc9457;
+        }
+
+        @Override
+        protected boolean isRFC9457Message(String response) {
+            return rfc9457;
+        }
+
+        @Override
+        protected int getStatusCode(String response) {
+            return 404;
+        }
+
+        @Override
+        protected String getReasonPhrase(String response) {
+            return "Not Found";
+        }
+
+        @Override
+        protected String getBody(String response) throws IOException {
+            bodyRead = true;
+            try (InputStream is = new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))) {
+                return readBody(is);
+            }
+        }
+
+        @Override
+        public void prepareRequest(Object request) {}
+    }
+
+    private static void throwBaseException(Integer statusCode, String reasonPhrase) throws IOException {
+        throw new IOException("base:" + statusCode);
+    }
+
     @Test
     void hasRFC9457ContentType() {
         RFC9457Reporter<Object, Exception, Object> reporter = new RFC9457Reporter<Object, Exception, Object>() {
@@ -53,4 +99,76 @@ class RFC9457ReporterTest {
         assertTrue(reporter.hasRFC9457ContentType("application/problem+json"));
         assertTrue(reporter.hasRFC9457ContentType("application/problem+json; charset=utf-8"));
     }
+
+    @Test
+    void readBodyTruncatesOversizedStream() throws IOException {
+        byte[] oversized = new byte[RFC9457Reporter.MAX_BODY_BYTES + 4096];
+        Arrays.fill(oversized, (byte) 'a');
+        ByteArrayInputStream is = new ByteArrayInputStream(oversized);
+        String body = RFC9457Reporter.readBody(is);
+        assertEquals(RFC9457Reporter.MAX_BODY_BYTES, body.length());
+        assertEquals(4096, is.available());
+    }
+
+    @Test
+    void readBodyReadsSmallStreamCompletely() throws IOException {
+        String payload = "{\"title\":\"gone\"}";
+        String body = RFC9457Reporter.readBody(new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(payload, body);
+    }
+
+    @Test
+    void validProblemDetailsThrowsHttpRFC9457Exception() {
+        TestReporter reporter = new TestReporter(true);
+        HttpRFC9457Exception exception = assertThrows(
+                HttpRFC9457Exception.class,
+                () -> reporter.generateException(
+                        "{\"status\":404,\"title\":\"not found\"}", RFC9457ReporterTest::throwBaseException));
+        assertEquals(404, exception.getStatusCode());
+        assertEquals("not found", exception.getPayload().getTitle());
+    }
+
+    @Test
+    void malformedProblemDetailsFallsBackToBaseException() {
+        TestReporter reporter = new TestReporter(true);
+        IOException exception = assertThrows(
+                IOException.class,
+                () -> reporter.generateException("{ this is not json", RFC9457ReporterTest::throwBaseException));
+        assertFalse(exception instanceof HttpRFC9457Exception);
+        assertEquals("base:404", exception.getMessage());
+    }
+
+    @Test
+    void oversizedProblemDetailsIsTruncatedAndFallsBackToBaseException() {
+        char[] padding = new char[RFC9457Reporter.MAX_BODY_BYTES];
+        Arrays.fill(padding, 'a');
+        // valid problem details if read completely, malformed once truncated at the cap
+        String oversized = "{\"title\":\"" + new String(padding) + "\"}";
+        TestReporter reporter = new TestReporter(true);
+        IOException exception = assertThrows(
+                IOException.class,
+                () -> reporter.generateException(oversized, RFC9457ReporterTest::throwBaseException));
+        assertFalse(exception instanceof HttpRFC9457Exception);
+        assertEquals("base:404", exception.getMessage());
+    }
+
+    @Test
+    void emptyBodyThrowsExceptionWithEmptyPayload() {
+        TestReporter reporter = new TestReporter(true);
+        HttpRFC9457Exception exception = assertThrows(
+                HttpRFC9457Exception.class,
+                () -> reporter.generateException("", RFC9457ReporterTest::throwBaseException));
+        assertSame(RFC9457Payload.INSTANCE, exception.getPayload());
+    }
+
+    @Test
+    void nonProblemDetailsResponseDoesNotReadBody() {
+        TestReporter reporter = new TestReporter(false);
+        IOException exception = assertThrows(
+                IOException.class,
+                () -> reporter.generateException("{\"title\":\"ignored\"}", RFC9457ReporterTest::throwBaseException));
+        assertFalse(exception instanceof HttpRFC9457Exception);
+        assertEquals("base:404", exception.getMessage());
+        assertFalse(reporter.bodyRead);
+    }
 }
diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
index 41f43701b..6f5453319 100644
--- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
+++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheRFC9457Reporter.java
@@ -19,14 +19,14 @@
 package org.eclipse.aether.transport.apache;
 
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import java.io.InputStream;
 
 import org.apache.http.Header;
+import org.apache.http.HttpEntity;
 import org.apache.http.HttpHeaders;
 import org.apache.http.HttpRequest;
 import org.apache.http.client.HttpResponseException;
 import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.util.EntityUtils;
 import org.eclipse.aether.spi.connector.transport.http.HttpConstants;
 import org.eclipse.aether.spi.connector.transport.http.RFC9457.RFC9457Reporter;
 
@@ -67,9 +67,16 @@ public class ApacheRFC9457Reporter extends RFC9457Reporter<CloseableHttpResponse
 
     @Override
     protected String getBody(final CloseableHttpResponse response) throws IOException {
-        if (response.getEntity() == null) {
+        HttpEntity entity = response.getEntity();
+        if (entity == null) {
             return "";
         }
-        return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        InputStream is = entity.getContent();
+        if (is == null) {
+            return "";
+        }
+        try (InputStream stream = is) {
+            return readBody(stream);
+        }
     }
 }
diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
index 34765c82e..0c36edf3c 100644
--- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
+++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
@@ -679,7 +679,9 @@ final class ApacheTransporter extends AbstractTransporter implements HttpTranspo
                 if (lastModifiedHeader != null) {
                     Date lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
                     if (lastModified != null) {
-                        pathProcessor.setLastModified(task.getDataPath(), lastModified.getTime());
+                        pathProcessor.setLastModified(
+                                task.getDataPath(),
+                                HttpTransporterUtils.clampRemoteLastModified(lastModified.getTime()));
                     }
                 }
             }
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java
index a2733ce6a..6340e57e4 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkRFC9457Reporter.java
@@ -23,7 +23,6 @@ import java.io.InputStream;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.Builder;
 import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
 import org.eclipse.aether.spi.connector.transport.http.HttpConstants;
@@ -64,7 +63,7 @@ public class JdkRFC9457Reporter
     @Override
     protected String getBody(final HttpResponse<InputStream> response) throws IOException {
         try (InputStream is = response.body()) {
-            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+            return readBody(is);
         }
     }
 }
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
index 2ce6137b8..b0851117e 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
@@ -377,9 +377,10 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
                     try {
                         pathProcessor.setLastModified(
                                 task.getDataPath(),
-                                ZonedDateTime.parse(lastModifiedHeader, RFC7231)
-                                        .toInstant()
-                                        .toEpochMilli());
+                                HttpTransporterUtils.clampRemoteLastModified(
+                                        ZonedDateTime.parse(lastModifiedHeader, RFC7231)
+                                                .toInstant()
+                                                .toEpochMilli()));
                     } catch (DateTimeParseException e) {
                         // fall through
                     }
diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java
index 08d5c7852..127d46c83 100644
--- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java
+++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyRFC9457Reporter.java
@@ -20,7 +20,6 @@ package org.eclipse.aether.transport.jetty;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -77,7 +76,7 @@ public class JettyRFC9457Reporter
     @Override
     protected String getBody(final InputStreamResponseListener listener) throws IOException {
         try (InputStream is = listener.getInputStream()) {
-            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+            return readBody(is);
         }
     }
 }
diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
index f2be78483..e7db9cf25 100644
--- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
+++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
@@ -312,7 +312,8 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
             long lastModified =
                     response.getHeaders().getDateField(LAST_MODIFIED); // note: Wagon also does first not last
             if (lastModified != -1) {
-                pathProcessor.setLastModified(task.getDataPath(), lastModified);
+                pathProcessor.setLastModified(
+                        task.getDataPath(), HttpTransporterUtils.clampRemoteLastModified(lastModified));
             }
         }
         Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
diff --git a/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
index 9f651c690..4273e5a77 100644
--- a/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
+++ b/maven-resolver-transport-url/src/main/java/org/eclipse/aether/transport/url/UrlTransporter.java
@@ -250,7 +250,8 @@ public class UrlTransporter extends AbstractTransporter implements HttpTransport
             if (task.getDataPath() != null) {
                 long lastModified = con.getLastModified();
                 if (lastModified != 0) {
-                    pathProcessor.setLastModified(task.getDataPath(), lastModified);
+                    pathProcessor.setLastModified(
+                            task.getDataPath(), HttpTransporterUtils.clampRemoteLastModified(lastModified));
                 }
             }
         } finally {
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
index f8759f3ad..1eba761aa 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/ChecksumUtils.java
@@ -39,6 +39,12 @@ import java.util.Map;
  */
 @Deprecated
 public final class ChecksumUtils {
+    /**
+     * Upper bound (in characters) for data read from a checksum file, see {@link #read(File)}. Every sane
+     * checksum file format fits well within this limit; longer input is rejected as malformed instead of being
+     * buffered into memory.
+     */
+    private static final int MAX_CHECKSUM_FILE_CHARS = 8192;
 
     private ChecksumUtils() {
         // hide constructor
@@ -54,23 +60,13 @@ public final class ChecksumUtils {
      */
     @Deprecated
     public static String read(File checksumFile) throws IOException {
-        String checksum = "";
+        String checksum;
         try (BufferedReader br = new BufferedReader(
                 new InputStreamReader(new FileInputStream(checksumFile), StandardCharsets.UTF_8), 512)) {
-            while (true) {
-                String line = br.readLine();
-                if (line == null) {
-                    break;
-                }
-                line = line.trim();
-                if (!line.isEmpty()) {
-                    checksum = line;
-                    break;
-                }
-            }
+            checksum = readFirstNonEmptyLine(br, checksumFile.toString());
         }
 
-        if (checksum.matches(".+= [0-9A-Fa-f]+")) {
+        if (isAlgorithmHeaderFormat(checksum)) {
             int lastSpacePos = checksum.lastIndexOf(' ');
             checksum = checksum.substring(lastSpacePos + 1);
         } else {
@@ -84,6 +80,52 @@ public final class ChecksumUtils {
         return checksum;
     }
 
+    /**
+     * Reads the first non-empty line, enforcing {@link #MAX_CHECKSUM_FILE_CHARS} on the total amount of data
+     * consumed. Returns the trimmed line, or an empty string if the stream holds no non-empty line.
+     */
+    private static String readFirstNonEmptyLine(BufferedReader reader, String source) throws IOException {
+        StringBuilder buffer = new StringBuilder(64);
+        int read = 0;
+        int c;
+        while ((c = reader.read()) != -1) {
+            if (++read > MAX_CHECKSUM_FILE_CHARS) {
+                throw new IOException("Checksum file " + source + " is malformed: longer than "
+                        + MAX_CHECKSUM_FILE_CHARS + " characters");
+            }
+            if (c == '\n' || c == '\r') {
+                String line = buffer.toString().trim();
+                if (!line.isEmpty()) {
+                    return line;
+                }
+                buffer.setLength(0);
+            } else {
+                buffer.append((char) c);
+            }
+        }
+        return buffer.toString().trim();
+    }
+
+    /**
+     * Non-backtracking equivalent of {@code line.matches(".+= [0-9A-Fa-f]+")}: at least one character, followed
+     * by "= ", followed by one or more hex digits reaching the end of the line ("&lt;algorithm&gt; (&lt;file&gt;)
+     * = &lt;hex&gt;" style checksum lines).
+     */
+    private static boolean isAlgorithmHeaderFormat(String line) {
+        int lastSpacePos = line.lastIndexOf(' ');
+        if (lastSpacePos < 2 || lastSpacePos == line.length() - 1 || line.charAt(lastSpacePos - 1) != '=') {
+            return false;
+        }
+        for (int i = lastSpacePos + 1; i < line.length(); i++) {
+            char ch = line.charAt(i);
+            boolean hex = (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
+            if (!hex) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Calculates checksums for the specified file.
      *
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
index b9251e19d..4b958288a 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtils.java
@@ -328,6 +328,25 @@ public final class HttpTransporterUtils {
                 ConfigurationProperties.HTTP_VERSION);
     }
 
+    /**
+     * Clamps a remote-supplied {@code Last-Modified} timestamp to the local clock: returns the given value as-is,
+     * or the current time when the value lies in the future.
+     * <p>
+     * HTTP transports stamp the downloaded file's modification time from the server-provided {@code Last-Modified}
+     * header, and the update check manager subsequently uses that file modification time as the {@code lastUpdated}
+     * instant for update-policy cutoffs. A repository serving a far-future {@code Last-Modified} date would otherwise
+     * permanently suppress interval-based update checks for that path. Update-interval decisions must rest on
+     * locally-observed time, so future-dated values are clamped to {@code System.currentTimeMillis()}.
+     *
+     * @param lastModifiedMillis the last modified timestamp in milliseconds since the epoch, as supplied by the
+     *                           remote server
+     * @return the supplied value, clamped to the local wall clock
+     * @since 2.0.23
+     */
+    public static long clampRemoteLastModified(long lastModifiedMillis) {
+        return Math.min(lastModifiedMillis, System.currentTimeMillis());
+    }
+
     /**
      * Shared code to create "base {@link URI}" for most common HTTP remote repositories and all HTTP transports.
      * Note: this method just applies common validation and adjustments to URI, but it does not enforce protocol
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtilsTest.java
index 4d3f8455e..93a1c917c 100644
--- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtilsTest.java
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/connector/transport/http/HttpTransporterUtilsTest.java
@@ -27,8 +27,28 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class HttpTransporterUtilsTest {
+    @Test
+    void clampRemoteLastModifiedPassesPastAndPresentValues() {
+        long testStart = System.currentTimeMillis();
+        long past = testStart - 10_000L;
+        assertEquals(past, HttpTransporterUtils.clampRemoteLastModified(past));
+        assertEquals(0L, HttpTransporterUtils.clampRemoteLastModified(0L));
+        assertEquals(-1L, HttpTransporterUtils.clampRemoteLastModified(-1L));
+    }
+
+    @Test
+    void clampRemoteLastModifiedClampsFutureValuesToLocalClock() {
+        long testStart = System.currentTimeMillis();
+        long future = testStart + 365L * 24L * 60L * 60L * 1000L; // one year ahead
+        long clamped = HttpTransporterUtils.clampRemoteLastModified(future);
+        assertTrue(clamped >= testStart, "clamped value must not predate the call");
+        assertTrue(clamped <= System.currentTimeMillis(), "clamped value must not exceed the local clock");
+        assertTrue(clamped < future, "future-dated value must be clamped");
+    }
+
     @Test
     void goodUris() throws URISyntaxException {
         URI uri;