001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.http;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021/**
022 * Neutralizes log-forging (log-injection) in client-controlled strings before they are written to a log line.
023 *
024 * <p>
025 * A remote client can place CR/LF (or Unicode line/paragraph separators, bidi controls, or other control characters)
026 * into a request URI, header name/value, or body, and — if that text is interpolated verbatim into a log record — forge
027 * additional physical log lines (e.g. a fake status banner) or corrupt a downstream log parser/SIEM. This utility
028 * replaces every such character with a <b>visible, inert escape</b> (<c>\r</c>, <c>\n</c>, or <c>\\uXXXX</c>) so the
029 * value's information is preserved (a reader can still see there <i>was</i> a newline) but it can no longer terminate
030 * the current line or start a new one.
031 *
032 * <p>
033 * The transform:
034 * <ul>
035 *    <li>CR (<c>\\u000D</c>) &rarr; <c>\r</c>, LF (<c>\\u000A</c>) &rarr; <c>\n</c>.
036 *    <li>All other C0 controls (<c>\\u0000</c>&ndash;<c>\\u001F</c>) <b>except TAB</b> (<c>\\u0009</c>) &rarr; <c>\\uXXXX</c>.
037 *    <li>DEL and C1 controls (<c>\\u007F</c>&ndash;<c>\\u009F</c>, which includes NEL <c>\\u0085</c>) &rarr; <c>\\uXXXX</c>.
038 *    <li>Line separator (<c>\\u2028</c>), paragraph separator (<c>\\u2029</c>), and bidi controls
039 *       (<c>\\u202A</c>&ndash;<c>\\u202E</c>, <c>\\u2066</c>&ndash;<c>\\u2069</c>) &rarr; <c>\\uXXXX</c>.
040 *    <li>TAB (<c>\\u0009</c>) is <b>preserved</b> &mdash; it cannot forge a line.
041 * </ul>
042 *
043 * <p>
044 * When a length cap is supplied, truncation is applied <b>after</b> escaping, counts <b>sanitized</b> (character) output,
045 * appends a <c>&hellip;[truncated]</c> marker, and never cuts an escape sequence or a surrogate pair in half. A fast path
046 * returns the original reference unchanged when the string contains no character requiring an escape and is within the
047 * cap, so well-behaved clients incur no allocation.
048 *
049 * <p>
050 * All methods are thread-safe (the utility is stateless).
051 *
052 * <h5 class='section'>See Also:</h5><ul>
053 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerLoggingAndDebugging">Logging / Debugging</a>
054 * </ul>
055 *
056 * @since 10.0.0
057 */
058public final class DebugTextSanitizer {
059
060   /** Marker appended when a value is truncated by the length cap. */
061   public static final String TRUNCATED_MARKER = "\u2026[truncated]";
062
063   private DebugTextSanitizer() {}
064
065   /**
066    * Escapes every log-forging control character in the given string, with no length cap.
067    *
068    * @param value The string to sanitize. Can be <jk>null</jk> (returns <jk>null</jk>).
069    * @return The sanitized string, or the original reference if nothing needed escaping. <jk>null</jk> if the input was
070    *    <jk>null</jk>.
071    */
072   public static String sanitize(String value) {
073      return sanitize(value, Integer.MAX_VALUE);
074   }
075
076   /**
077    * Escapes every log-forging control character in the given string, then caps the <i>sanitized</i> output to
078    * {@code maxLen} characters.
079    *
080    * <p>
081    * The cap counts the escaped (rendered) length, appends {@code …[truncated]} when it clips, and never splits an
082    * escape sequence or a surrogate pair.
083    *
084    * @param value The string to sanitize. Can be <jk>null</jk> (returns <jk>null</jk>).
085    * @param maxLen The maximum number of sanitized characters to keep (excluding the truncation marker).
086    * @return The sanitized (and possibly truncated) string, or the original reference if nothing needed escaping and it
087    *    was within the cap. <jk>null</jk> if the input was <jk>null</jk>.
088    */
089   @SuppressWarnings({
090      "java:S3776" // Sanitizer encodes a control-character state machine; complexity is inherent.
091   })
092   public static String sanitize(String value, int maxLen) {
093      if (value == null)
094         return null;
095
096      // Fast path: nothing to escape and already within the cap → return the original reference (no allocation).
097      var clean = value.length() <= maxLen;
098      if (clean) {
099         for (var i = 0; i < value.length(); i++) {
100            if (needsEscape(value.charAt(i))) {
101               clean = false;
102               break;
103            }
104         }
105      }
106      if (clean)
107         return value;
108
109      var sb = new StringBuilder(Math.min(value.length(), Math.max(maxLen, 0)) + 16);
110      var truncated = false;
111      var n = value.length();
112      var i = 0;
113      while (i < n) {
114         var c = value.charAt(i);
115         String token;
116         int consumed;
117         if (needsEscape(c)) {
118            token = escape(c);
119            consumed = 1;
120         } else if (Character.isHighSurrogate(c) && i + 1 < n && Character.isLowSurrogate(value.charAt(i + 1))) {
121            // Keep a surrogate pair together so the cap never cuts a code point in half.
122            token = value.substring(i, i + 2);
123            consumed = 2;
124         } else {
125            token = String.valueOf(c);
126            consumed = 1;
127         }
128         if (sb.length() + token.length() > maxLen) {
129            truncated = true;
130            break;
131         }
132         sb.append(token);
133         i += consumed;
134      }
135      if (truncated)
136         sb.append(TRUNCATED_MARKER);
137      return sb.toString();
138   }
139
140   private static boolean needsEscape(char c) {
141      if (c == '\t')
142         return false;
143      if (c <= '\u001F')  // C0 controls (CR/LF included).
144         return true;
145      if (c >= '\u007F' && c <= '\u009F')  // DEL + C1 controls (includes NEL \u0085).
146         return true;
147      if (c == '\u2028' || c == '\u2029')  // Line / paragraph separators.
148         return true;
149      if (c >= '\u202A' && c <= '\u202E')  // Bidi embedding/override controls.
150         return true;
151      return c >= '\u2066' && c <= '\u2069';  // Bidi isolate controls.
152   }
153
154   private static String escape(char c) {
155      if (c == '\r')
156         return "\\r";
157      if (c == '\n')
158         return "\\n";
159      return f("\\u%04X", (int) c);
160   }
161}