Validate challenge tokens before use

pull/81/head
Richard Körber 2019-01-12 16:28:07 +01:00
parent 3abf56e325
commit 9d3ab4972c
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
5 changed files with 95 additions and 4 deletions

View File

@ -23,6 +23,7 @@ import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.lang.JoseException; import org.jose4j.lang.JoseException;
import org.shredzone.acme4j.Login; import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSON;
/** /**
@ -51,7 +52,11 @@ public class TokenChallenge extends Challenge {
* Gets the token. * Gets the token.
*/ */
protected String getToken() { protected String getToken() {
return getJSON().get(KEY_TOKEN).asString(); String token = getJSON().get(KEY_TOKEN).asString();
if (!AcmeUtils.isValidBase64Url(token)) {
throw new AcmeProtocolException("Invalid token: " + token);
}
return token;
} }
/** /**

View File

@ -82,8 +82,6 @@ public class DefaultConnection implements Connection {
private static final String MIME_JSON_PROBLEM = "application/problem+json"; private static final String MIME_JSON_PROBLEM = "application/problem+json";
private static final String MIME_CERTIFICATE_CHAIN = "application/pem-certificate-chain"; private static final String MIME_CERTIFICATE_CHAIN = "application/pem-certificate-chain";
private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+");
private static final URI BAD_NONCE_ERROR = URI.create("urn:ietf:params:acme:error:badNonce"); private static final URI BAD_NONCE_ERROR = URI.create("urn:ietf:params:acme:error:badNonce");
private static final int MAX_ATTEMPTS = 10; private static final int MAX_ATTEMPTS = 10;
@ -235,7 +233,7 @@ public class DefaultConnection implements Connection {
return null; return null;
} }
if (!BASE64URL_PATTERN.matcher(nonceHeader).matches()) { if (!AcmeUtils.isValidBase64Url(nonceHeader)) {
throw new AcmeProtocolException("Invalid replay nonce: " + nonceHeader); throw new AcmeProtocolException("Invalid replay nonce: " + nonceHeader);
} }

View File

@ -69,6 +69,8 @@ public final class AcmeUtils {
private static final Pattern MAIL_PATTERN = Pattern.compile("\\?|@.*,"); private static final Pattern MAIL_PATTERN = Pattern.compile("\\?|@.*,");
private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]*");
private static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64, private static final Base64.Encoder PEM_ENCODER = Base64.getMimeEncoder(64,
"\n".getBytes(StandardCharsets.US_ASCII)); "\n".getBytes(StandardCharsets.US_ASCII));
@ -156,6 +158,20 @@ public final class AcmeUtils {
return Base64Url.decode(base64); return Base64Url.decode(base64);
} }
/**
* Validates that the given {@link String} is a valid base64url encoded value.
*
* @param base64
* {@link String} to validate
* @return {@code true}: String contains a valid base64url encoded value.
* {@code false} if the {@link String} was {@code null} or contained illegal
* characters.
* @since 2.6
*/
public static boolean isValidBase64Url(@Nullable String base64) {
return base64 != null && BASE64URL_PATTERN.matcher(base64).matches();
}
/** /**
* ASCII encodes a domain name. * ASCII encodes a domain name.
* <p> * <p>

View File

@ -0,0 +1,56 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2019 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j.challenge;
import static org.junit.Assert.fail;
import java.io.IOException;
import org.junit.Test;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.JSONBuilder;
/**
* Unit tests for {@link TokenChallenge}.
*/
public class TokenChallengeTest {
/**
* Test that invalid tokens are detected.
*/
@Test
public void testInvalidToken() throws IOException {
TestableConnectionProvider provider = new TestableConnectionProvider();
Login login = provider.createLogin();
JSONBuilder jb = new JSONBuilder();
jb.put("url", "https://example.com/acme/1234");
jb.put("type", "generic");
jb.put("token", "<script>someMaliciousCode()</script>");
TokenChallenge challenge = new TokenChallenge(login, jb.toJSON());
try {
challenge.getToken();
fail("Invalid token was accepted");
} catch (AcmeProtocolException ex) {
// expected
}
provider.close();
}
}

View File

@ -92,6 +92,22 @@ public class AcmeUtilsTest {
assertThat(base64UrlDecode, is(sha256hash("foobar"))); assertThat(base64UrlDecode, is(sha256hash("foobar")));
} }
/**
* Test base64 URL validation.
*/
@Test
public void testBase64UrlValidate() {
assertThat(isValidBase64Url(null), is(false));
assertThat(isValidBase64Url(""), is(true));
assertThat(isValidBase64Url(" "), is(false));
assertThat(isValidBase64Url("Zg"), is(true));
assertThat(isValidBase64Url("Zg="), is(false));
assertThat(isValidBase64Url("Zg=="), is(false));
assertThat(isValidBase64Url("Zm9v"), is(true));
assertThat(isValidBase64Url(" Zm9v "), is(false));
assertThat(isValidBase64Url("<some>.illegal#Text"), is(false));
}
/** /**
* Test ACE conversion. * Test ACE conversion.
*/ */