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>) → <c>\r</c>, LF (<c>\\u000A</c>) → <c>\n</c>. 036 * <li>All other C0 controls (<c>\\u0000</c>–<c>\\u001F</c>) <b>except TAB</b> (<c>\\u0009</c>) → <c>\\uXXXX</c>. 037 * <li>DEL and C1 controls (<c>\\u007F</c>–<c>\\u009F</c>, which includes NEL <c>\\u0085</c>) → <c>\\uXXXX</c>. 038 * <li>Line separator (<c>\\u2028</c>), paragraph separator (<c>\\u2029</c>), and bidi controls 039 * (<c>\\u202A</c>–<c>\\u202E</c>, <c>\\u2066</c>–<c>\\u2069</c>) → <c>\\uXXXX</c>. 040 * <li>TAB (<c>\\u0009</c>) is <b>preserved</b> — 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>…[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}