From 721eb42cc5ed36c54d655f73cd3d0df111b1e41d Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Mon, 27 Jul 2026 21:16:42 -0700 Subject: [PATCH] fix: keep credentials out of HTTP errors --- .../agent/publisher/HttpPublisher.java | 23 ++++++++--- .../agent/publisher/HttpPublisherTest.java | 39 +++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/gpuflight/agent/publisher/HttpPublisher.java b/src/main/java/com/gpuflight/agent/publisher/HttpPublisher.java index 695b9ca..48a320e 100644 --- a/src/main/java/com/gpuflight/agent/publisher/HttpPublisher.java +++ b/src/main/java/com/gpuflight/agent/publisher/HttpPublisher.java @@ -71,7 +71,7 @@ public boolean publish(String topic, String key, LogWrapper log) { Thread.currentThread().interrupt(); return false; } catch (Exception e) { - System.out.println("HTTP Connection error: " + e); + logConnectionError("HTTP event", e); return false; } } @@ -88,7 +88,7 @@ public boolean publishStream(String sessionId, List ndjsonLines) { + " session=" + sessionId + " lines=" + ndjsonLines.size() + " gzipBytes=" + body.length); return postGzStream(sessionId, body); } catch (Exception e) { - System.out.println("HTTP stream connection error: " + e); + logConnectionError("HTTP stream", e); return false; } } @@ -149,7 +149,7 @@ private boolean postGzStream(String sessionId, byte[] gzBody) { Thread.currentThread().interrupt(); return false; } catch (Exception e) { - System.out.println("HTTP stream connection error: " + e); + logConnectionError("HTTP stream", e); return false; } } @@ -188,17 +188,30 @@ public boolean publishSessionComplete(String sessionId) { Thread.currentThread().interrupt(); return false; } catch (Exception e) { - System.out.println("HTTP session-complete connection error: " + e); + logConnectionError("HTTP session-complete", e); return false; } } private void addAuthHeader(HttpRequest.Builder requestBuilder) { if (config.authToken() != null && !config.authToken().isBlank()) { - requestBuilder.header("Authorization", "Bearer " + config.authToken()); + // Environment/config-file values can acquire surrounding CR/LF or + // whitespace while crossing shells. API keys never contain them, + // and passing them through makes HttpClient reject the header. + requestBuilder.header("Authorization", "Bearer " + config.authToken().trim()); } } + private static void logConnectionError(String operation, Exception error) { + // HttpClient's IllegalArgumentException includes the rejected header + // value. Printing the exception/message here can therefore disclose a + // complete Authorization bearer token. The exception type is enough + // to distinguish malformed requests from transport failures without + // putting credentials in logs. + System.out.println(operation + " connection error (" + + error.getClass().getSimpleName() + ")"); + } + private static byte[] gzip(byte[] content) throws Exception { var out = new ByteArrayOutputStream(); try (var gzip = new GZIPOutputStream(out)) { diff --git a/src/test/java/com/gpuflight/agent/publisher/HttpPublisherTest.java b/src/test/java/com/gpuflight/agent/publisher/HttpPublisherTest.java index 24d81f0..5fb8fb2 100644 --- a/src/test/java/com/gpuflight/agent/publisher/HttpPublisherTest.java +++ b/src/test/java/com/gpuflight/agent/publisher/HttpPublisherTest.java @@ -8,7 +8,9 @@ import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.List; @@ -110,6 +112,43 @@ void publish_sendsAuthorizationHeader() throws InterruptedException { assertEquals("Bearer gpfl_tok123", lastAuthHeader.get()); } + @Test + void publishStream_trimsTransportWhitespaceFromAuthorizationToken() { + HttpConfig config = new HttpConfig(hostUrl(), "v1", " \tgpfl_tok123\r\n", 5, + "stream", 100, 1_000_000L); + HttpPublisher pub = new HttpPublisher(config); + + boolean ok = pub.publishStream("session-1", + List.of("{\"type\":\"kernel_event\",\"session_id\":\"session-1\"}")); + + assertTrue(ok); + assertEquals("Bearer gpfl_tok123", lastAuthHeader.get()); + } + + @Test + void publishStream_malformedAuthorizationNeverLeaksTokenInLogs() throws IOException { + String secret = "gpfl_secret\rleak"; + HttpConfig config = new HttpConfig(hostUrl(), "v1", secret, 5, + "stream", 100, 1_000_000L); + HttpPublisher pub = new HttpPublisher(config); + PrintStream originalOut = System.out; + var captured = new ByteArrayOutputStream(); + boolean ok; + try { + System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8)); + ok = pub.publishStreamGz("session-1", + gzipBytes("{\"type\":\"kernel_event\"}\n")); + } finally { + System.setOut(originalOut); + } + + String output = captured.toString(StandardCharsets.UTF_8); + assertFalse(ok); + assertTrue(output.contains("IllegalArgumentException")); + assertFalse(output.contains(secret)); + assertFalse(output.contains("Bearer gpfl_secret")); + } + @Test void publish_noToken_doesNotSendAuthHeader() throws InterruptedException { HttpConfig config = new HttpConfig(hostUrl(), "v1", null, 5);