JWE Vulnerability: Cannot force Content type to be JWT when processing an encrypted JWT

Issue #222 invalid
Former user created an issue

When using the DefaultJWTProcessor to process an encryptedJWT, I cannot configure it to make sure that the nested payload is a JWT. If I passed a plain JSON object to the JWE object and encrypt it, the JWT processor will successfully return the claims even though it is configured with an expected signature algorithm for validating a signed JWT. As a result, if the encryption algorithm/key is compromised, the attacker will be able to pass whatever he wants without being caught by the JWT signature validation.

The vulnerability is in the method process(EncryptedJWT encryptedJWT, C context) of DefaultJWTProcessor.java:

            if("JWT".equalsIgnoreCase(encryptedJWT.getHeader().getContentType())) {
                SignedJWT nestedJWT = encryptedJWT.getPayload().toSignedJWT();
                if(nestedJWT == null) {
                    throw INVALID_NESTED_JWT_EXCEPTION;
                } else {
                    return this.process(nestedJWT, context);
                }
            } else {
                return this.verifyAndReturnClaims(encryptedJWT, context);
            }

As you can see, if encryptedJWT.getHeader().getContentType() is not "JWT", then it will directly return the claims. As a workaround, I can override the method process(EncryptedJWT encryptedJWT, C context) in order to check if encryptedJWT.getHeader().getContentType() returns "JWT". If not, I will fail the validation. But I think there should be a way to enforce the content type for the nested payload of an encrypted JWT.

Comments (4)

  1. Vladimir Dzhuvinov

    Hi,

    Thanks for this report.

    According to the JWT spec the cty must be present in the nested case. I'll update the code so that an INVALID_NESTED_JWT_EXCEPTION gets thrown in that case.

  2. Vladimir Dzhuvinov

    Added a few tests, but couldn't reproduce the issue with v4.37.

    Here is the commit with the tests: 1378273

    To reopen the issue please submit a test case.

  3. Peter Young

    Ok, I modified your test case to set content type "ANY". I also pass the result from the call plainJWT.getPayload() to the constructor of JWEObject, and the test failed because the JWTProcessor didn't catch it. This means if an attacker compromises JWE, JWS will also be bypassed without further effort. It should be possible to enforce the nested payload content type to be JWT as opposed to anything set by the sender (attacker).

    @Test
    public void testRejectPlainNestedJWT_anyCTY()
            throws Exception {
    
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
                .privateKey(keyPair.getPrivate())
                .keyID("1")
                .build();
    
        KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSha256");
        SecretKey hmacKey = keyGenerator.generateKey();
        OctetSequenceKey hmacJWK = new OctetSequenceKey.Builder(hmacKey).build();
    
        DefaultJWTProcessor proc = new DefaultJWTProcessor();
        proc.setJWEKeySelector(new JWEDecryptionKeySelector(
                JWEAlgorithm.RSA_OAEP_256,
                EncryptionMethod.A128GCM,
                new ImmutableJWKSet(new JWKSet(rsaJWK))
        ));
        proc.setJWSKeySelector(new JWSVerificationKeySelector(
                JWSAlgorithm.HS256,
                new ImmutableJWKSet(new JWKSet(hmacJWK))
        ));
    
        PlainJWT plainJWT = new PlainJWT(new JWTClaimsSet.Builder().subject("alice").build());
    
        JWEObject jweObject = new JWEObject(
                new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM)
                    .contentType("ANY")
                    .build(),
                    plainJWT.getPayload());
        jweObject.encrypt(new RSAEncrypter(rsaJWK));
    
        String jwe = jweObject.serialize();
    
        try {
            proc.process(jwe, null);
            fail();
        } catch (BadJWTException e) {
            assertEquals("Payload of JWE object is not a valid JSON object", e.getMessage());
        }
    }
    
  4. Log in to comment