Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ public static ValidationResult validateSyntax(ACMEIdentifier id) {
* _policy_ is checked elsewhere.
*/
private static ValidationResult validateSyntaxDNS(String value) {

// dns identifiers must be hostnames, not IP literals (RFC 8555).
// Accepting IPs enables SSRF via HTTP-01 (see dogtagpki/pki#5407).
if (isIpLiteral(value)) {
ACMEError error = new ACMEError();
error.setType("urn:ietf:params:acme:error:malformed");
error.setDetail("DNS identifier must not be an IP address: " + value);
return ValidationResult.fail(error);
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
String[] labels = value.split("\\.");

if (labels.length < 1) {
Expand Down Expand Up @@ -109,6 +119,113 @@ private static ValidationResult validateSyntaxDNS(String value) {
return ValidationResult.ok();
}

/**
* True if value is an IPv4/IPv6 literal (optional [], optional leading "*.").
* Wildcard is stripped because newOrder removes "*." before HTTP-01.
* Parsing is lexical only (no InetAddress/DNS) and covers the decimal IPv4
* forms Java accepts (d, d.d, d.d.d, d.d.d.d).
*/
static boolean isIpLiteral(String value) {
String host = value.startsWith("*.") ? value.substring(2) : value;
if (host.startsWith("[") && host.endsWith("]") && host.length() > 2) {
host = host.substring(1, host.length() - 1);
}
if (host.indexOf(':') >= 0) {
return isIpv6Literal(host);
}
return isIpv4Literal(host);
}

/**
* Match Java Inet4Address decimal IPv4 text forms (historical inet_aton style):
* d (32-bit), d.d (8+24), d.d.d (8+8+16), or d.d.d.d (four bytes).
* Digits only; leading zeros allowed. See java.net.Inet4Address.
*/
Comment thread
agaragna77 marked this conversation as resolved.
private static boolean isIpv4Literal(String host) {
if (host.isEmpty()) {
return false;
}
for (int i = 0; i < host.length(); i++) {
char c = host.charAt(i);
if (c != '.' && (c < '0' || c > '9')) {
return false;
}
}
String[] parts = host.split("\\.", -1);
if (parts.length < 1 || parts.length > 4) {
return false;
}
for (String part : parts) {
if (part.isEmpty()) {
return false;
}
}
switch (parts.length) {
case 1: // d -> 32-bit value (e.g. 2130706433)
return inRange(parts[0], 0xFFFFFFFFL);
case 2: // d.d -> 8-bit + 24-bit (e.g. 127.1)
return inRange(parts[0], 255) && inRange(parts[1], 0xFFFFFFL);
case 3: // d.d.d -> 8-bit + 8-bit + 16-bit (e.g. 127.0.1)
return inRange(parts[0], 255) && inRange(parts[1], 255)
&& inRange(parts[2], 0xFFFFL);
case 4: // d.d.d.d -> four 8-bit octets (e.g. 127.0.0.1)
return inRange(parts[0], 255) && inRange(parts[1], 255)
&& inRange(parts[2], 255) && inRange(parts[3], 255);
default:
return false;
}
}

private static boolean inRange(String part, long max) {
try {
long value = Long.parseLong(part);
return value >= 0 && value <= max;
} catch (NumberFormatException e) {
return false;
}
}

/**
* Simplified lexical IPv6 text check (RFC 4291 / RFC 5952 style):
* hex digits and ':', at most one "::" compression, optional dotted IPv4
* tail for mapped forms (e.g. ::ffff:127.0.0.1). Not a full RFC parser.
*/
Comment thread
agaragna77 marked this conversation as resolved.
private static boolean isIpv6Literal(String host) {
int colons = 0;
int doubleColon = 0;
for (int i = 0; i < host.length(); i++) {
char c = host.charAt(i);
if (c == ':') {
colons++;
// "::" may appear at most once
if (i + 1 < host.length() && host.charAt(i + 1) == ':') {
doubleColon++;
i++;
colons++;
}
} else if (!isHex(c) && c != '.') {
// '.' only for IPv4-mapped / IPv4-compatible tails
return false;
}
}
// At least one ":" pair worth of colons; reject multiple "::"
if (colons < 2 || doubleColon > 1) {
return false;
}
// If an IPv4 tail is present after the last ':', validate it as IPv4
int lastColon = host.lastIndexOf(':');
if (lastColon >= 0 && host.indexOf('.', lastColon) >= 0) {
return isIpv4Literal(host.substring(lastColon + 1));
}
return true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private static boolean isHex(char c) {
return c >= '0' && c <= '9'
|| c >= 'a' && c <= 'f'
|| c >= 'A' && c <= 'F';
}

/* helper predicates for "dns" identifier validity */
private static boolean isLetter(char c) { return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; }
private static boolean isDigit(char c) { return c >= '0' && c <= '9'; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,15 @@ public ValidationResult validateChallenge(

if (response == null || !response.equals(keyAuthorization)) {

// Log the body for operators; never return it to the ACME client
// (SSRF response disclosure — dogtagpki/pki#5407).
logger.error("Invalid response: " + response);

ACMEError error = new ACMEError();
error.setType("urn:ietf:params:acme:error:incorrectResponse");
error.setDetail(
"Unable to validate HTTP-01 challenge at " + validationURL + "\n" +
"Incorrect response: " + response);
"Unable to validate HTTP-01 challenge at " + validationURL + ": "
+ "response did not match the expected key authorization");
Comment thread
agaragna77 marked this conversation as resolved.

return ValidationResult.fail(error);
}
Expand All @@ -110,22 +112,19 @@ public String getResponse(URI validationURL) throws Exception {

logger.info("Retrieving " + validationURL);

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(validationURL);
CloseableHttpResponse httpResponse = httpClient.execute(httpGet);

String response;
try {
HttpEntity entity = httpResponse.getEntity();
response = IOUtils.toString(entity.getContent(), "UTF-8").trim();
EntityUtils.consume(entity);

} finally {
httpResponse.close();
// Do not follow redirects: a controlled first hop can otherwise
// redirect validation to internal HTTP targets (dogtagpki/pki#5407).
try (CloseableHttpClient httpClient = HttpClients.custom()
.disableRedirectHandling()
.build()) {
HttpGet httpGet = new HttpGet(validationURL);
try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) {
Comment thread
agaragna77 marked this conversation as resolved.
HttpEntity entity = httpResponse.getEntity();
String response = IOUtils.toString(entity.getContent(), "UTF-8").trim();
EntityUtils.consume(entity);
Comment thread
agaragna77 marked this conversation as resolved.
Comment thread
agaragna77 marked this conversation as resolved.
logger.info("Response: " + response);
return response;
}
}

logger.info("Response: " + response);

return response;
}
}
Loading