diff --git a/openam-federation/openam-federation-library/src/main/java/com/sun/identity/plugin/log/impl/FedletLogger.java b/openam-federation/openam-federation-library/src/main/java/com/sun/identity/plugin/log/impl/FedletLogger.java index dda2f6d75f..c0feaa127a 100644 --- a/openam-federation/openam-federation-library/src/main/java/com/sun/identity/plugin/log/impl/FedletLogger.java +++ b/openam-federation/openam-federation-library/src/main/java/com/sun/identity/plugin/log/impl/FedletLogger.java @@ -24,6 +24,7 @@ * * $Id: FedletLogger.java,v 1.3 2008/08/06 17:28:14 exu Exp $ * + * Portions Copyrighted 2026 3A Systems LLC. */ package com.sun.identity.plugin.log.impl; @@ -124,20 +125,58 @@ public void access( } } - private static String formatMessage(String messageId, String[] param, + static String formatMessage(String messageId, String[] param, Object session) { if ((param == null) || (param.length == 0)) { return messageId; } else { for (int i = 0; i < param.length; i++) { - messageId = messageId + "\n{" + param[i] + "}"; + messageId = messageId + "\n{" + escapeLineBreaks(param[i]) + "}"; } if (session != null) { - messageId = messageId + "\n{" + session.toString() + "}"; + messageId = messageId + "\n{" + escapeLineBreaks(session.toString()) + "}"; } return messageId; } - } + } + + /** + * Each parameter is written on a braced line of its own; a line break inside one is + * request data and is written as the escape it stands for, so it cannot leave the braces + * and start a line that reads as another record. Every character {@code \R} matches counts + * as a line break, as it does for the debug file; a backslash is escaped too, so that an + * escape cannot be told from the same text logged as it is. + */ + private static String escapeLineBreaks(String value) { + if (value == null) { + return null; + } + StringBuilder out = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + switch (c) { + case '\\': + out.append("\\\\"); + break; + case '\r': + out.append("\\r"); + break; + case '\n': + out.append("\\n"); + break; + case '\u000B': + case '\f': + case '\u0085': + case '\u2028': + case '\u2029': + out.append(String.format("\\u%04X", (int) c)); + break; + default: + out.append(c); + } + } + return out.toString(); + } /** * Logs error messages to the error logs. diff --git a/openam-federation/openam-federation-library/src/test/java/com/sun/identity/plugin/log/impl/FedletLoggerTest.java b/openam-federation/openam-federation-library/src/test/java/com/sun/identity/plugin/log/impl/FedletLoggerTest.java new file mode 100644 index 0000000000..d70c50cc3e --- /dev/null +++ b/openam-federation/openam-federation-library/src/test/java/com/sun/identity/plugin/log/impl/FedletLoggerTest.java @@ -0,0 +1,63 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package com.sun.identity.plugin.log.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.testng.annotations.Test; + +public class FedletLoggerTest { + + @Test + public void everyParameterGoesOnItsOwnBracedLine() { + assertThat(FedletLogger.formatMessage("LOGIN_SUCCESS", new String[] {"alice", "sp"}, "sess")) + .isEqualTo("LOGIN_SUCCESS\n{alice}\n{sp}\n{sess}"); + } + + /** A line break inside a parameter must not leave the braces and start a line of its own. */ + @Test + public void aLineBreakInAParameterIsWrittenAsAnEscape() { + assertThat(FedletLogger.formatMessage("LOGIN_FAILED", new String[] {"alice\r\nSEVERE: forged\rx\ny"}, null)) + .isEqualTo("LOGIN_FAILED\n{alice\\r\\nSEVERE: forged\\rx\\ny}"); + } + + /** The session is the assertion's principal name, request data like the parameters. */ + @Test + public void aLineBreakInTheSessionIsWrittenAsAnEscape() { + assertThat(FedletLogger.formatMessage("LOGIN_FAILED", new String[] {"alice"}, "bob\nSEVERE: forged")) + .isEqualTo("LOGIN_FAILED\n{alice}\n{bob\\nSEVERE: forged}"); + } + + @Test + public void everyKindOfLineBreakIsWrittenAsAnEscape() { + assertThat(FedletLogger.formatMessage("LOGIN_FAILED", + new String[] {"a\u0085b\u2028c\u2029d\u000Be\ff"}, null)) + .isEqualTo("LOGIN_FAILED\n{a\\u0085b\\u2028c\\u2029d\\u000Be\\u000Cf}"); + } + + /** Otherwise a literal backslash-n in a value and a line break would be logged alike. */ + @Test + public void aBackslashIsEscapedToo() { + assertThat(FedletLogger.formatMessage("LOGIN_FAILED", new String[] {"a\\nb", "a\nb"}, null)) + .isEqualTo("LOGIN_FAILED\n{a\\\\nb}\n{a\\nb}"); + } + + @Test + public void aMessageWithoutParametersIsTheMessageId() { + assertThat(FedletLogger.formatMessage("LOGIN_SUCCESS", null, null)).isEqualTo("LOGIN_SUCCESS"); + assertThat(FedletLogger.formatMessage("LOGIN_SUCCESS", new String[0], "sess")).isEqualTo("LOGIN_SUCCESS"); + } +} diff --git a/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugFileImpl.java b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugFileImpl.java index 15b8f1296e..89189d634d 100644 --- a/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugFileImpl.java +++ b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugFileImpl.java @@ -28,6 +28,7 @@ /** * Portions Copyrighted 2014-2016 ForgeRock AS. + * Portions Copyrighted 2026 3A Systems LLC. */ package com.sun.identity.shared.debug.file.impl; @@ -44,7 +45,6 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; -import java.io.StringWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -142,18 +142,7 @@ private boolean isConfigFileInitialized() { @Override public void writeIt(String prefix, String msg, Throwable th) throws IOException { - StringBuilder buf = new StringBuilder(); - buf.append(prefix); - buf.append('\n'); - buf.append(msg); - if (th != null) { - buf.append('\n'); - StringWriter stBuf = new StringWriter(DebugConstants.MAX_BUFFER_SIZE_EXCEPTION); - PrintWriter stackStream = new PrintWriter(stBuf); - th.printStackTrace(stackStream); - stackStream.flush(); - buf.append(stBuf.toString()); - } + String record = DebugRecordFormat.format(prefix, msg, th); if (isConfigChanged() || !isConfigFileInitialized()) { initialize(); @@ -166,7 +155,7 @@ public void writeIt(String prefix, String msg, Throwable th) throws IOException fileLock.readLock().lock(); try { if (debugWriter != null) { - debugWriter.println(buf.toString()); + debugWriter.println(record); } else { StdDebugFile.printError(prefix, msg, th); } diff --git a/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormat.java b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormat.java new file mode 100644 index 0000000000..c4d5905c79 --- /dev/null +++ b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormat.java @@ -0,0 +1,92 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package com.sun.identity.shared.debug.file.impl; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.regex.Pattern; + +import com.sun.identity.shared.debug.DebugConstants; + +/** + * Lays out one debug record: the prefix line, the message, and the stack trace if there is + * one. A record is told from the next by its prefix line starting at column 0, followed by + * the first line of the message, also at column 0; nothing else that is logged may start a + * line there: every line break inside the message and every line of the stack trace (an + * exception's message is logged data too) continues the record indented, and a line break + * inside the prefix (the transaction id can come from a request header) is written as a + * space. A single-line message, the usual case, is written exactly as it always was, and a + * multi-line dump stays readable, indented. + *
+ * This is what keeps a value taken from a request - a user name, a RelayState, a SAML
+ * attribute - from forging a record of its own, whether it reaches the debug file through
+ * {@code Debug} directly or through the SLF4J binding.
+ */
+public final class DebugRecordFormat {
+
+ /** What every continued line is indented with. */
+ static final String CONTINUATION = " ";
+
+ private static final Pattern LINE_BREAK = Pattern.compile("\\R");
+
+ private DebugRecordFormat() {
+ }
+
+ /**
+ * @param prefix the record's prefix line (debug name, timestamp, thread, transaction)
+ * @param msg the message; {@code null} is written as {@code null}
+ * @param th the throwable whose stack trace follows the message, or {@code null}
+ * @return the record, without a trailing line break
+ */
+ public static String format(String prefix, String msg, Throwable th) {
+ StringBuilder buf = new StringBuilder(LINE_BREAK.matcher(prefix).replaceAll(" "));
+ buf.append('\n');
+ buf.append(continued(String.valueOf(msg), false));
+ if (th != null) {
+ StringWriter trace = new StringWriter(DebugConstants.MAX_BUFFER_SIZE_EXCEPTION);
+ PrintWriter writer = new PrintWriter(trace);
+ th.printStackTrace(writer);
+ writer.flush();
+ buf.append('\n');
+ buf.append(continued(trace.toString(), true));
+ }
+ return buf.toString();
+ }
+
+ /**
+ * {@code text} with every line break turned into a newline followed by the continuation
+ * indent, the first line indented as well when {@code indentFirst}; trailing line breaks
+ * are dropped.
+ */
+ private static String continued(String text, boolean indentFirst) {
+ String[] lines = LINE_BREAK.split(text, -1);
+ int last = lines.length;
+ while (last > 1 && lines[last - 1].isEmpty()) {
+ last--;
+ }
+ StringBuilder out = new StringBuilder(text.length() + last * CONTINUATION.length());
+ for (int i = 0; i < last; i++) {
+ if (i > 0) {
+ out.append('\n');
+ }
+ if (i > 0 || indentFirst) {
+ out.append(CONTINUATION);
+ }
+ out.append(lines[i]);
+ }
+ return out.toString();
+ }
+}
diff --git a/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/StdDebugFile.java b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/StdDebugFile.java
index 08efe48df3..0823292b92 100644
--- a/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/StdDebugFile.java
+++ b/openam-shared/src/main/java/com/sun/identity/shared/debug/file/impl/StdDebugFile.java
@@ -12,17 +12,16 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package com.sun.identity.shared.debug.file.impl;
import static org.forgerock.openam.utils.Time.*;
-import com.sun.identity.shared.debug.DebugConstants;
import com.sun.identity.shared.debug.file.DebugFile;
import java.io.IOException;
import java.io.PrintWriter;
-import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -33,9 +32,17 @@ public class StdDebugFile implements DebugFile {
private static final StdDebugFile INSTANCE = new StdDebugFile();
- private PrintWriter stdoutWriter = new PrintWriter(System.out, true);
+ private final PrintWriter stdoutWriter;
private StdDebugFile() {
+ this(new PrintWriter(System.out, true));
+ }
+
+ /**
+ * @param stdoutWriter where the records are written; the tests pass their own
+ */
+ StdDebugFile(PrintWriter stdoutWriter) {
+ this.stdoutWriter = stdoutWriter;
}
/**
@@ -49,18 +56,7 @@ public static StdDebugFile getInstance() {
@Override
public void writeIt(String prefix, String msg, Throwable th) throws IOException {
- StringBuilder buf = new StringBuilder(prefix);
- buf.append('\n');
- buf.append(msg);
- if (th != null) {
- buf.append('\n');
- StringWriter stBuf = new StringWriter(DebugConstants.MAX_BUFFER_SIZE_EXCEPTION);
- PrintWriter stackStream = new PrintWriter(stBuf);
- th.printStackTrace(stackStream);
- stackStream.flush();
- buf.append(stBuf.toString());
- }
- stdoutWriter.println(buf.toString());
+ stdoutWriter.println(DebugRecordFormat.format(prefix, msg, th));
}
/**
@@ -72,13 +68,9 @@ public void writeIt(String prefix, String msg, Throwable th) throws IOException
*/
public static void printError(String debugName, String message, Throwable ex) {
SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss:SSS a zzz");
- String prefix = debugName + ":" + dateFormat.format(newDate()) + ": " + Thread.currentThread().toString() +
- "\n";
+ String prefix = debugName + ":" + dateFormat.format(newDate()) + ": " + Thread.currentThread().toString();
- System.err.println(prefix + message);
- if (ex != null) {
- ex.printStackTrace(System.err);
- }
+ System.err.println(DebugRecordFormat.format(prefix, message, ex));
}
}
diff --git a/openam-shared/src/test/java/com/sun/identity/shared/debug/DebugTest.java b/openam-shared/src/test/java/com/sun/identity/shared/debug/DebugTest.java
index b365d3f2bc..48de0b28b4 100644
--- a/openam-shared/src/test/java/com/sun/identity/shared/debug/DebugTest.java
+++ b/openam-shared/src/test/java/com/sun/identity/shared/debug/DebugTest.java
@@ -12,10 +12,14 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package com.sun.identity.shared.debug;
import com.sun.identity.shared.configuration.SystemPropertiesManager;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -113,4 +117,20 @@ public void shouldFindFileWithWildCard() throws Exception {
checkLogFileStatus(true, "wildcardTest");
}
+ /** A line break carried in a logged value continues the record indented in the debug file. */
+ @Test
+ public void shouldContinueALineBreakInTheMessageIndented() throws Exception {
+ // given
+ initializeProvider(DEBUG_CONFIG_FOR_TEST);
+ IDebug debug = provider.getInstance(logName);
+
+ // when
+ debug.error("user\n" + logName + ":01/01/2026 00:00:00:000 AM UTC: Thread[main,5,main]\nforged", null);
+
+ // then
+ String log = new String(Files.readAllBytes(Paths.get(debugDirectory, logName)), StandardCharsets.UTF_8);
+ Assert.assertTrue(log.contains("ERROR: user\n " + logName + ":01/01/2026"), log);
+ Assert.assertTrue(log.contains("\n forged"), log);
+ }
+
}
diff --git a/openam-shared/src/test/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormatTest.java b/openam-shared/src/test/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormatTest.java
new file mode 100644
index 0000000000..7f8083dd0d
--- /dev/null
+++ b/openam-shared/src/test/java/com/sun/identity/shared/debug/file/impl/DebugRecordFormatTest.java
@@ -0,0 +1,81 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package com.sun.identity.shared.debug.file.impl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.testng.annotations.Test;
+
+public class DebugRecordFormatTest {
+
+ private static final String PREFIX = "amAuth:09/18/2026 10:00:00:000 AM MSK: Thread[main,5,main]: TransactionId[x]";
+
+ @Test
+ public void aSingleLineMessageIsWrittenAsBefore() {
+ assertThat(DebugRecordFormat.format(PREFIX, "login failed for user", null))
+ .isEqualTo(PREFIX + "\nlogin failed for user");
+ }
+
+ /** A line break carried in a logged value cannot start a line that reads as a new record. */
+ @Test
+ public void aLineBreakInTheMessageContinuesTheRecordIndented() {
+ String forged = "user\n" + PREFIX + "\nforged message";
+
+ assertThat(DebugRecordFormat.format(PREFIX, forged, null))
+ .isEqualTo(PREFIX + "\nuser\n " + PREFIX + "\n forged message");
+ }
+
+ @Test
+ public void everyKindOfLineBreakIsAContinuation() {
+ assertThat(DebugRecordFormat.format(PREFIX, "a\r\nb\rc\u2028d\u2029e\u0085f\u000Bg\fh", null))
+ .isEqualTo(PREFIX + "\na\n b\n c\n d\n e\n f\n g\n h");
+ }
+
+ /** The prefix carries the transaction id, which can be taken from a request header. */
+ @Test
+ public void aLineBreakInThePrefixIsWrittenAsASpace() {
+ assertThat(DebugRecordFormat.format("amAuth: TransactionId[a\u0085forged\r\nb]", "failed", null))
+ .isEqualTo("amAuth: TransactionId[a forged b]\nfailed");
+ }
+
+ @Test
+ public void aMultiLineDumpStaysReadable() {
+ assertThat(DebugRecordFormat.format(PREFIX, "SAML response:\n