{"record":{"id":"187358f40dd01173","repo":"spring-projects/spring-security","slug":"expected-low-surrogate-character-but-found-value","errorCode":null,"errorMessage":"Expected low surrogate character but found value = <low>","messagePattern":"Expected low surrogate character but found value = <low>","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"web/src/main/java/org/springframework/security/web/util/TextEscapeUtils.java","lineNumber":61,"sourceCode":"\t\t\t\tsb.append(\"&gt;\");\n\t\t\t}\n\t\t\telse if (ch == '&') {\n\t\t\t\tsb.append(\"&amp;\");\n\t\t\t}\n\t\t\telse if (Character.isWhitespace(ch)) {\n\t\t\t\tsb.append(\"&#\").append((int) ch).append(\";\");\n\t\t\t}\n\t\t\telse if (Character.isISOControl(ch)) {\n\t\t\t\t// ignore control chars\n\t\t\t}\n\t\t\telse if (Character.isHighSurrogate(ch)) {\n\t\t\t\tif (i + 1 >= s.length()) {\n\t\t\t\t\t// Unexpected end\n\t\t\t\t\tthrow new IllegalArgumentException(\"Missing low surrogate character at end of string\");\n\t\t\t\t}\n\t\t\t\tchar low = s.charAt(i + 1);\n\t\t\t\tif (!Character.isLowSurrogate(low)) {\n\t\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\t\"Expected low surrogate character but found value = \" + (int) low);\n\t\t\t\t}\n\t\t\t\tint codePoint = Character.toCodePoint(ch, low);\n\t\t\t\tif (Character.isDefined(codePoint)) {\n\t\t\t\t\tsb.append(\"&#\").append(codePoint).append(\";\");\n\t\t\t\t}\n\t\t\t\ti++; // skip the next character as we have already dealt with it\n\t\t\t}\n\t\t\telse if (Character.isLowSurrogate(ch)) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unexpected low surrogate character, value = \" + (int) ch);\n\t\t\t}\n\t\t\telse if (Character.isDefined(ch)) {\n\t\t\t\tsb.append(\"&#\").append((int) ch).append(\";\");\n\t\t\t}\n\t\t\t// Ignore anything else\n\t\t}\n\t\treturn sb.toString();\n\t}","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/web/src/main/java/org/springframework/security/web/util/TextEscapeUtils.java#L43-L79","documentation":"TextEscapeUtils.escapeEntities() throws this when it encounters a high surrogate whose immediately following character exists but is not a valid low surrogate — the pair cannot be combined into a Unicode code point. The numeric char value of the offending character is included in the message.","triggerScenarios":"Calling escapeEntities(s) where s contains a high surrogate (U+D800–U+DBFF) followed by a non-surrogate char (e.g. '\\uD83D' followed by 'a'), produced by corrupted UTF-16 data, buggy character-level edits, or concatenated fragments of different strings.","commonSituations":"Manually assembled strings where an emoji was copied without its second char; regex/matcher results cut at surrogate boundaries; strings mutated by tools unaware of UTF-16 pairs.","solutions":["Fix the upstream code that corrupts surrogate pairs — never split or reassemble strings between the two halves of a pair.","Sanitize the input first: remove or replace lone surrogates (e.g. with a regex or CharsetEncoder REPLACE) before calling escapeEntities.","If the char is expected, use Character.toCodePoint / codePointAt-based processing so pairs travel together."],"exampleFix":"// before\nString escaped = TextEscapeUtils.escapeEntities(corruptedInput);\n// after\nString clean = corruptedInput.codePoints()\n    .filter(cp -> !Character.isSurrogate(cp))\n    .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)\n    .toString();\nString escaped = TextEscapeUtils.escapeEntities(clean);","handlingStrategy":"validation","validationCode":"public static boolean hasNoLoneSurrogates(String s) {\n    for (int i = 0; i < s.length(); i++) {\n        char c = s.charAt(i);\n        if (Character.isHighSurrogate(c)) {\n            if (i + 1 >= s.length() || !Character.isLowSurrogate(s.charAt(i + 1))) return false;\n            i++;\n        } else if (Character.isLowSurrogate(c)) return false;\n    }\n    return true;\n}\n","typeGuard":"public static String dropMalformedPairs(String s) {\n    return s.codePoints().filter(cp -> !Character.isSurrogate(cp))\n        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();\n}\n","tryCatchPattern":"try {\n    escaped = TextEscapeUtils.escapeEntities(input);\n} catch (IllegalArgumentException e) {\n    if (!e.getMessage().startsWith(\"Expected low surrogate\") && !e.getMessage().startsWith(\"Missing low surrogate\")\n        && !e.getMessage().startsWith(\"Unexpected low surrogate\")) throw e;\n    escaped = TextEscapeUtils.escapeEntities(dropMalformedPairs(input));\n}\n","preventionTips":["Never concatenate or slice strings in the middle of a surrogate pair","Use String.codePoints() or codePointAt() when inspecting/transforming characters","Sanitize decoded input with CodingErrorAction.REPLACE at the charset level","Log the offending char value from the message to trace the corruption source"],"tags":["java","string-encoding","unicode","xml-escaping"],"backgroundTag":"invalid-argument-format","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}