From 077084ce816a32f52768a85adc233baf9a4b477a Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Tue, 20 Oct 2026 11:39:16 +0100 Subject: [PATCH 1/2] Add MISSING alerts for character-filtering callables --- .../CWE-532/CharacterFilteringTest.java | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java diff --git a/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java b/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java new file mode 100644 index 000000000000..257e916a7547 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java @@ -0,0 +1,136 @@ +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import org.apache.logging.log4j.Logger; + +class CharacterFilteringTest { + private final Logger logger = null; + + void testStringBuilder(String password) { + logger.info(removeNewlines(password)); // $ MISSING: Alert + } + + static String removeNewlines(String input) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char current = input.charAt(i); + if (current != '\n' && current != '\r') { + result.append(current); + } + } + return result.toString(); + } + + void testReplacement(String authToken) { + logger.info(escapeControlCharacters(authToken)); // $ MISSING: Alert + } + + static String escapeControlCharacters(String input) { + StringBuilder result = new StringBuilder(); + for (char current : input.toCharArray()) { + switch (current) { + case '\n': + result.append("\\n"); + break; + case '\r': + result.append("\\r"); + break; + default: + result.append(current); + break; + } + } + return result.toString(); + } + + void testWriterParameter(String accessToken) throws IOException { + logger.info(copyPrintableCodePoints(accessToken)); // $ MISSING: Alert + } + + static String copyPrintableCodePoints(String input) throws IOException { + StringWriter output = new StringWriter(); + copyPrintableCodePoints(input, output); + return output.toString(); + } + + static void copyPrintableCodePoints(CharSequence input, Writer output) throws IOException { + int position = 0; + while (position < input.length()) { + int codePoint = Character.codePointAt(input, position); + if (!Character.isISOControl(codePoint)) { + char[] characters = Character.toChars(codePoint); + output.write(characters, 0, characters.length); + } + position += Character.charCount(codePoint); + } + } + + void testOutputArray(String clientSecret) { + logger.info(copyAsciiCharacters(clientSecret)); // $ MISSING: Alert + } + + static String copyAsciiCharacters(String input) { + char[] output = new char[input.length()]; + int outputPosition = 0; + for (int inputPosition = 0; inputPosition < input.length(); inputPosition++) { + char current = input.charAt(inputPosition); + if (current >= 0x20 && current < 0x7f) { + output[outputPosition++] = current; + } + } + return new String(output, 0, outputPosition); + } + + void testConcatenation(String apiSecret) { + logger.info(removeWhitespace(apiSecret)); // $ MISSING: Alert + } + + static String removeWhitespace(String input) { + String result = ""; + for (int i = 0; i < input.length(); i++) { + char current = input.charAt(i); + if (!Character.isWhitespace(current)) { + result += current; + } + } + return result; + } + + void testReaderWriter(String sessionToken) throws IOException { + logger.info(copyWithoutLineBreaks(sessionToken)); // $ MISSING: Alert + } + + static String copyWithoutLineBreaks(String input) throws IOException { + StringReader reader = new StringReader(input); + StringWriter writer = new StringWriter(); + int current; + while ((current = reader.read()) != -1) { + if (current != '\n' && current != '\r') { + writer.write(current); + } + } + return writer.toString(); + } + + void testNonPreservingTransformations(String password, String authToken) { + logger.info(maskCharacters(password)); // Safe + logger.info(sumCharacters(authToken)); // Safe + } + + static String maskCharacters(String input) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + result.append('*'); + } + return result.toString(); + } + + static String sumCharacters(String input) { + int sum = 0; + for (int i = 0; i < input.length(); i++) { + sum += input.charAt(i); + } + return Integer.toString(sum); + } +} From e0106810e6096f2b625838499e3e5662cde9fe8a Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Tue, 20 Oct 2026 12:23:06 +0100 Subject: [PATCH 2/2] Add characterFilteringCallStep to java/sensitive-log --- .../code/java/dataflow/CharacterFiltering.qll | 163 ++++++++++++++++++ .../java/security/SensitiveLoggingQuery.qll | 5 + .../CWE-532/CharacterFilteringTest.java | 12 +- .../CWE-532/SensitiveLogInfo.expected | 43 ++++- 4 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 java/ql/lib/semmle/code/java/dataflow/CharacterFiltering.qll diff --git a/java/ql/lib/semmle/code/java/dataflow/CharacterFiltering.qll b/java/ql/lib/semmle/code/java/dataflow/CharacterFiltering.qll new file mode 100644 index 000000000000..d7171651dd1f --- /dev/null +++ b/java/ql/lib/semmle/code/java/dataflow/CharacterFiltering.qll @@ -0,0 +1,163 @@ +/** + * Provides support for identifying callables that copy characters from an input string while + * possibly dropping or replacing some characters. + */ +overlay[local?] +module; + +import java +import semmle.code.java.dataflow.DataFlow + +private predicate isStringLike(Type type) { + type.(RefType).getASupertype*().hasQualifiedName("java.lang", "CharSequence") +} + +private predicate isReader(Type type) { + type.(RefType).getASupertype*().hasQualifiedName("java.io", "Reader") +} + +private predicate isCharacterOutput(Type type) { + type.(Array).getElementType().(PrimitiveType).hasName("char") + or + type.(RefType).getASupertype*().hasQualifiedName("java.lang", "Appendable") + or + type.(RefType).getASupertype*().hasQualifiedName("java.io", "Writer") +} + +private predicate occursInLoop(Expr expr, LoopStmt loop) { + expr.getEnclosingStmt().getEnclosingStmt*() = loop +} + +/** + * Holds if `read` is a character or code point read from `input` during `loop`. + */ +private predicate readsCharacter(Parameter input, LoopStmt loop, Expr read) { + exists(MethodCall call, VarAccess inputAccess | + inputAccess.getVariable() = input and + DataFlow::localExprFlow(inputAccess, call.getQualifier()) and + call.getMethod().hasName("charAt") and + read = call and + occursInLoop(call, loop) + ) + or + exists(MethodCall call, VarAccess inputAccess | + inputAccess.getVariable() = input and + DataFlow::localExprFlow(inputAccess, call.getArgument(0)) and + call.getMethod().hasQualifiedName("java.lang", "Character", "codePointAt") and + read = call and + occursInLoop(call, loop) + ) + or + exists(EnhancedForStmt forStmt, MethodCall toCharArray, VarAccess inputAccess | + loop = forStmt and + forStmt.getExpr() = toCharArray and + toCharArray.getMethod().hasName("toCharArray") and + inputAccess.getVariable() = input and + DataFlow::localExprFlow(inputAccess, toCharArray.getQualifier()) and + read = forStmt.getVariable().getVariable().getAnAccess() and + occursInLoop(read, loop) + ) + or + exists( + LocalVariableDecl reader, ClassInstanceExpr readerInit, Expr constructorArgument, + VarAccess inputAccess, MethodCall readCall + | + reader.getType() = readerInit.getType() and + isReader(reader.getType()) and + reader.getInitializer() = readerInit and + constructorArgument = readerInit.getAnArgument() and + inputAccess.getVariable() = input and + DataFlow::localExprFlow(inputAccess, constructorArgument) and + readCall.getQualifier() = reader.getAnAccess() and + readCall.getMethod().hasName("read") and + read = readCall and + occursInLoop(readCall, loop) + ) +} + +/** + * Holds if a character value can flow from `source` to `sink`. + * + * This is deliberately separate from normal taint tracking: character conversion is used only as + * evidence for inferring a string-level summary. + */ +private predicate characterFlowStep(DataFlow::Node source, DataFlow::Node sink) { + DataFlow::localFlowStep(source, sink) + or + exists(MethodCall conversion | + conversion.getMethod().hasQualifiedName("java.lang", "Character", ["toChars", "toString"]) and + source.asExpr() = conversion.getArgument(0) and + sink.asExpr() = conversion + ) +} + +private predicate characterFlows(Expr source, Expr sink) { + characterFlowStep*(DataFlow::exprNode(source), DataFlow::exprNode(sink)) +} + +/** + * Holds if `output` receives a character derived from `read` during `loop`. + */ +private predicate writesCharacter(LoopStmt loop, Expr read, Variable output) { + exists(MethodCall write, Expr value | + occursInLoop(write, loop) and + write.getMethod().hasName(["append", "write"]) and + write.getQualifier() = output.getAnAccess() and + isCharacterOutput(output.getType()) and + value = write.getArgument(0) and + characterFlows(read, value) + ) + or + exists(AssignExpr write, ArrayAccess element | + occursInLoop(write, loop) and + write.getDest() = element and + element.getArray() = output.getAnAccess() and + isCharacterOutput(output.getType()) and + characterFlows(read, write.getSource()) + ) + or + exists(AssignAddExpr append | + occursInLoop(append, loop) and + append.getDest() = output.getAnAccess() and + output.getType() instanceof TypeString and + characterFlows(read, append.getSource()) + ) +} + +private predicate returnsOutput(Method method, Variable output) { + exists(ReturnStmt returnStmt | + returnStmt.getEnclosingCallable() = method and + returnStmt.getExpr().getAChildExpr*() = output.getAnAccess() + ) +} + +private predicate characterFilteringOutput( + Method method, int inputPosition, LocalScopeVariable output +) { + exists(Parameter input, LoopStmt loop, Expr read | + input = method.getParameter(inputPosition) and + isStringLike(input.getType()) and + loop.getEnclosingCallable() = method and + output.getCallable() = method + | + readsCharacter(input, loop, read) and + writesCharacter(loop, read, output) + ) +} + +/** + * Holds if a call to a character-filtering method copies data from `input` to `output`. + */ +predicate characterFilteringCallStep(DataFlow::Node input, DataFlow::Node output) { + exists(MethodCall call, int inputPosition, LocalScopeVariable filteredOutput | + characterFilteringOutput(call.getMethod(), inputPosition, filteredOutput) and + input.asExpr() = call.getArgument(inputPosition) + | + returnsOutput(call.getMethod(), filteredOutput) and output.asExpr() = call + or + exists(int outputPosition | + filteredOutput = call.getMethod().getParameter(outputPosition) and + output.asExpr() = call.getArgument(outputPosition) + ) + ) +} diff --git a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll index f35cae0e67f3..3d069917d74b 100644 --- a/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll +++ b/java/ql/lib/semmle/code/java/security/SensitiveLoggingQuery.qll @@ -3,6 +3,7 @@ import java private import semmle.code.java.dataflow.ExternalFlow import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.dataflow.CharacterFiltering import semmle.code.java.security.SensitiveActions import semmle.code.java.frameworks.android.Compose private import semmle.code.java.security.Sanitizers @@ -138,6 +139,10 @@ module SensitiveLoggerConfig implements DataFlow::ConfigSig { predicate isBarrierIn(DataFlow::Node node) { isSource(node) } + predicate isAdditionalFlowStep(DataFlow::Node source, DataFlow::Node sink) { + characterFilteringCallStep(source, sink) + } + predicate observeDiffInformedIncrementalMode() { any() } } diff --git a/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java b/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java index 257e916a7547..cb9693e24180 100644 --- a/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java +++ b/java/ql/test/query-tests/security/CWE-532/CharacterFilteringTest.java @@ -8,7 +8,7 @@ class CharacterFilteringTest { private final Logger logger = null; void testStringBuilder(String password) { - logger.info(removeNewlines(password)); // $ MISSING: Alert + logger.info(removeNewlines(password)); // $ Alert } static String removeNewlines(String input) { @@ -23,7 +23,7 @@ static String removeNewlines(String input) { } void testReplacement(String authToken) { - logger.info(escapeControlCharacters(authToken)); // $ MISSING: Alert + logger.info(escapeControlCharacters(authToken)); // $ Alert } static String escapeControlCharacters(String input) { @@ -45,7 +45,7 @@ static String escapeControlCharacters(String input) { } void testWriterParameter(String accessToken) throws IOException { - logger.info(copyPrintableCodePoints(accessToken)); // $ MISSING: Alert + logger.info(copyPrintableCodePoints(accessToken)); // $ Alert } static String copyPrintableCodePoints(String input) throws IOException { @@ -67,7 +67,7 @@ static void copyPrintableCodePoints(CharSequence input, Writer output) throws IO } void testOutputArray(String clientSecret) { - logger.info(copyAsciiCharacters(clientSecret)); // $ MISSING: Alert + logger.info(copyAsciiCharacters(clientSecret)); // $ Alert } static String copyAsciiCharacters(String input) { @@ -83,7 +83,7 @@ static String copyAsciiCharacters(String input) { } void testConcatenation(String apiSecret) { - logger.info(removeWhitespace(apiSecret)); // $ MISSING: Alert + logger.info(removeWhitespace(apiSecret)); // $ Alert } static String removeWhitespace(String input) { @@ -98,7 +98,7 @@ static String removeWhitespace(String input) { } void testReaderWriter(String sessionToken) throws IOException { - logger.info(copyWithoutLineBreaks(sessionToken)); // $ MISSING: Alert + logger.info(copyWithoutLineBreaks(sessionToken)); // $ Alert } static String copyWithoutLineBreaks(String input) throws IOException { diff --git a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected index 1155e4fb991e..6b703e625882 100644 --- a/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected +++ b/java/ql/test/query-tests/security/CWE-532/SensitiveLogInfo.expected @@ -1,4 +1,10 @@ #select +| CharacterFilteringTest.java:11:21:11:44 | removeNewlines(...) | CharacterFilteringTest.java:11:36:11:43 | password : String | CharacterFilteringTest.java:11:21:11:44 | removeNewlines(...) | This $@ is written to a log file. | CharacterFilteringTest.java:11:36:11:43 | password | potentially sensitive information | +| CharacterFilteringTest.java:26:21:26:54 | escapeControlCharacters(...) | CharacterFilteringTest.java:26:45:26:53 | authToken : String | CharacterFilteringTest.java:26:21:26:54 | escapeControlCharacters(...) | This $@ is written to a log file. | CharacterFilteringTest.java:26:45:26:53 | authToken | potentially sensitive information | +| CharacterFilteringTest.java:48:21:48:56 | copyPrintableCodePoints(...) | CharacterFilteringTest.java:48:45:48:55 | accessToken : String | CharacterFilteringTest.java:48:21:48:56 | copyPrintableCodePoints(...) | This $@ is written to a log file. | CharacterFilteringTest.java:48:45:48:55 | accessToken | potentially sensitive information | +| CharacterFilteringTest.java:70:21:70:53 | copyAsciiCharacters(...) | CharacterFilteringTest.java:70:41:70:52 | clientSecret : String | CharacterFilteringTest.java:70:21:70:53 | copyAsciiCharacters(...) | This $@ is written to a log file. | CharacterFilteringTest.java:70:41:70:52 | clientSecret | potentially sensitive information | +| CharacterFilteringTest.java:86:21:86:47 | removeWhitespace(...) | CharacterFilteringTest.java:86:38:86:46 | apiSecret : String | CharacterFilteringTest.java:86:21:86:47 | removeWhitespace(...) | This $@ is written to a log file. | CharacterFilteringTest.java:86:38:86:46 | apiSecret | potentially sensitive information | +| CharacterFilteringTest.java:101:21:101:55 | copyWithoutLineBreaks(...) | CharacterFilteringTest.java:101:43:101:54 | sessionToken : String | CharacterFilteringTest.java:101:21:101:55 | copyWithoutLineBreaks(...) | This $@ is written to a log file. | CharacterFilteringTest.java:101:43:101:54 | sessionToken | potentially sensitive information | | Test.java:11:21:11:53 | ... + ... | Test.java:11:46:11:53 | password : String | Test.java:11:21:11:53 | ... + ... | This $@ is written to a log file. | Test.java:11:46:11:53 | password | potentially sensitive information | | Test.java:12:22:12:52 | ... + ... | Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | This $@ is written to a log file. | Test.java:12:44:12:52 | authToken | potentially sensitive information | | Test.java:21:22:21:75 | ... + ... | Test.java:21:44:21:52 | authToken : String | Test.java:21:22:21:75 | ... + ... | This $@ is written to a log file. | Test.java:21:44:21:52 | authToken | potentially sensitive information | @@ -13,11 +19,23 @@ | Test.java:81:21:81:44 | ... + ... | Test.java:81:33:81:44 | refreshToken : String | Test.java:81:21:81:44 | ... + ... | This $@ is written to a log file. | Test.java:81:33:81:44 | refreshToken | potentially sensitive information | | Test.java:82:21:82:43 | ... + ... | Test.java:82:33:82:43 | secretValue : String | Test.java:82:21:82:43 | ... + ... | This $@ is written to a log file. | Test.java:82:33:82:43 | secretValue | potentially sensitive information | edges +| CharacterFilteringTest.java:11:36:11:43 | password : String | CharacterFilteringTest.java:11:21:11:44 | removeNewlines(...) | provenance | Config Sink:MaD:2 | +| CharacterFilteringTest.java:26:45:26:53 | authToken : String | CharacterFilteringTest.java:26:21:26:54 | escapeControlCharacters(...) | provenance | Config Sink:MaD:2 | +| CharacterFilteringTest.java:48:45:48:55 | accessToken : String | CharacterFilteringTest.java:48:21:48:56 | copyPrintableCodePoints(...) | provenance | Config Sink:MaD:2 | +| CharacterFilteringTest.java:48:45:48:55 | accessToken : String | CharacterFilteringTest.java:51:43:51:54 | input : String | provenance | | +| CharacterFilteringTest.java:51:43:51:54 | input : String | CharacterFilteringTest.java:53:33:53:37 | input : String | provenance | | +| CharacterFilteringTest.java:53:33:53:37 | input : String | CharacterFilteringTest.java:53:40:53:45 | output : StringWriter | provenance | Config | +| CharacterFilteringTest.java:53:40:53:45 | output : StringWriter | CharacterFilteringTest.java:54:16:54:21 | output : StringWriter | provenance | | +| CharacterFilteringTest.java:53:40:53:45 | output : StringWriter | CharacterFilteringTest.java:54:16:54:32 | toString(...) : String | provenance | %StringWriter | +| CharacterFilteringTest.java:54:16:54:21 | output : StringWriter | CharacterFilteringTest.java:54:16:54:32 | toString(...) : String | provenance | MaD:3 | +| CharacterFilteringTest.java:70:41:70:52 | clientSecret : String | CharacterFilteringTest.java:70:21:70:53 | copyAsciiCharacters(...) | provenance | Config Sink:MaD:2 | +| CharacterFilteringTest.java:86:38:86:46 | apiSecret : String | CharacterFilteringTest.java:86:21:86:47 | removeWhitespace(...) | provenance | Config Sink:MaD:2 | +| CharacterFilteringTest.java:101:43:101:54 | sessionToken : String | CharacterFilteringTest.java:101:21:101:55 | copyWithoutLineBreaks(...) | provenance | Config Sink:MaD:2 | | Test.java:11:46:11:53 | password : String | Test.java:11:21:11:53 | ... + ... | provenance | Sink:MaD:2 | | Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | provenance | Sink:MaD:1 | -| Test.java:21:44:21:52 | authToken : String | Test.java:21:44:21:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:21:44:21:52 | authToken : String | Test.java:21:44:21:67 | substring(...) : String | provenance | MaD:4 | | Test.java:21:44:21:67 | substring(...) : String | Test.java:21:22:21:75 | ... + ... | provenance | Sink:MaD:1 | -| Test.java:22:44:22:52 | authToken : String | Test.java:22:44:22:67 | substring(...) : String | provenance | MaD:3 | +| Test.java:22:44:22:52 | authToken : String | Test.java:22:44:22:67 | substring(...) : String | provenance | MaD:4 | | Test.java:22:44:22:67 | substring(...) : String | Test.java:22:22:22:75 | ... + ... | provenance | Sink:MaD:1 | | Test.java:31:30:31:37 | password : String | Test.java:31:21:31:37 | ... + ... | provenance | Sink:MaD:2 | | Test.java:75:33:75:43 | accessToken : String | Test.java:75:21:75:43 | ... + ... | provenance | Sink:MaD:2 | @@ -31,8 +49,26 @@ edges models | 1 | Sink: org.apache.logging.log4j; Logger; true; error; (String); ; Argument[0]; log-injection; manual | | 2 | Sink: org.apache.logging.log4j; Logger; true; info; (String); ; Argument[0]; log-injection; manual | -| 3 | Summary: java.lang; String; false; substring; ; ; Argument[this]; ReturnValue; taint; manual | +| 3 | Summary: java.io; StringWriter; false; toString; (); ; Argument[this]; ReturnValue; taint; manual | +| 4 | Summary: java.lang; String; false; substring; ; ; Argument[this]; ReturnValue; taint; manual | nodes +| CharacterFilteringTest.java:11:21:11:44 | removeNewlines(...) | semmle.label | removeNewlines(...) | +| CharacterFilteringTest.java:11:36:11:43 | password : String | semmle.label | password : String | +| CharacterFilteringTest.java:26:21:26:54 | escapeControlCharacters(...) | semmle.label | escapeControlCharacters(...) | +| CharacterFilteringTest.java:26:45:26:53 | authToken : String | semmle.label | authToken : String | +| CharacterFilteringTest.java:48:21:48:56 | copyPrintableCodePoints(...) | semmle.label | copyPrintableCodePoints(...) | +| CharacterFilteringTest.java:48:45:48:55 | accessToken : String | semmle.label | accessToken : String | +| CharacterFilteringTest.java:51:43:51:54 | input : String | semmle.label | input : String | +| CharacterFilteringTest.java:53:33:53:37 | input : String | semmle.label | input : String | +| CharacterFilteringTest.java:53:40:53:45 | output : StringWriter | semmle.label | output : StringWriter | +| CharacterFilteringTest.java:54:16:54:21 | output : StringWriter | semmle.label | output : StringWriter | +| CharacterFilteringTest.java:54:16:54:32 | toString(...) : String | semmle.label | toString(...) : String | +| CharacterFilteringTest.java:70:21:70:53 | copyAsciiCharacters(...) | semmle.label | copyAsciiCharacters(...) | +| CharacterFilteringTest.java:70:41:70:52 | clientSecret : String | semmle.label | clientSecret : String | +| CharacterFilteringTest.java:86:21:86:47 | removeWhitespace(...) | semmle.label | removeWhitespace(...) | +| CharacterFilteringTest.java:86:38:86:46 | apiSecret : String | semmle.label | apiSecret : String | +| CharacterFilteringTest.java:101:21:101:55 | copyWithoutLineBreaks(...) | semmle.label | copyWithoutLineBreaks(...) | +| CharacterFilteringTest.java:101:43:101:54 | sessionToken : String | semmle.label | sessionToken : String | | Test.java:11:21:11:53 | ... + ... | semmle.label | ... + ... | | Test.java:11:46:11:53 | password : String | semmle.label | password : String | | Test.java:12:22:12:52 | ... + ... | semmle.label | ... + ... | @@ -62,3 +98,4 @@ nodes | Test.java:82:21:82:43 | ... + ... | semmle.label | ... + ... | | Test.java:82:33:82:43 | secretValue : String | semmle.label | secretValue : String | subpaths +| CharacterFilteringTest.java:48:45:48:55 | accessToken : String | CharacterFilteringTest.java:51:43:51:54 | input : String | CharacterFilteringTest.java:54:16:54:32 | toString(...) : String | CharacterFilteringTest.java:48:21:48:56 | copyPrintableCodePoints(...) |