import java.nio.charset.StandardCharsets; import javax.xml.bind.DatatypeConverter; import org.owasp.esapi.codecs.PercentCodec; import org.owasp.esapi.codecs.PushbackSequence; import org.owasp.esapi.codecs.PushbackString; /** * The standard {@link PercentCodec} doesn't account for non BMP * characters which may be represented by more than 1 hexadecimal * value. The class below works around that. */ public class CustomPercentCodec extends PercentCodec { @Override public String decode(String input) { StringBuilder sb = new StringBuilder(); PushbackSequence pbs = new PushbackString(input); while (pbs.hasNext()) { String c = decodeHex(pbs); if (c != null) { sb.append(c); } else { sb.append(pbs.next()); } } return sb.toString(); } public String decodeHex( PushbackSequence input ) { StringBuilder hexCode = new StringBuilder(); while( hasNextEncodedCharacter( input ) ) { appendHexDigits( input, hexCode ); if( !isEvenNumberOfHexDigits( hexCode ) ) { input.reset(); return null; } } if( !isEvenNumberOfHexDigits( hexCode ) ) { input.reset(); return null; } return convertHexToString( hexCode.toString() ); } private Boolean hasNextEncodedCharacter( PushbackSequence input ) { input.mark(); Character first = input.next(); if ( first == null ) { input.reset(); return false; } // if this is not an encoded character, return null if( first != '%' ) { input.reset(); return false; } return true; } private boolean isEvenNumberOfHexDigits( StringBuilder hexCode ) { return isNotEmpty( hexCode ) && hexCode.toString().length() % 2 == 0; } private boolean isNotEmpty( StringBuilder hexCode ) { return hexCode.toString().length() > 0; } private String convertHexToString( String finalHexCode ) { return new String( DatatypeConverter.parseHexBinary( finalHexCode ), StandardCharsets.UTF_8 ); } private void appendHexDigits( PushbackSequence input, StringBuilder hexCode ) { for( int i = 0; i < 2; i++ ) { Character c = input.nextHex(); if( c != null ) { hexCode.append( c ); } } } }