Skip to content
Open
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
7 changes: 7 additions & 0 deletions core/src/main/java/google/registry/config/RegistryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,13 @@ public static String provideToolsClientSecret(RegistryConfigSettings config) {
return config.registryTool.clientSecret;
}

/** Secret key used to sign EPP session cookies (SESSION_INFO). */
@Provides
@Config("sessionSecret")
public static String provideSessionSecret(RegistryConfigSettings config) {
return config.misc.sessionSecret;
}

@Provides
@Config("rdapTos")
public static ImmutableList<String> provideRdapTos(RegistryConfigSettings config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ public static class Misc {
public String spec11OutgoingEmailAddress;
public List<String> spec11BccEmailAddresses;
public int transientFailureRetries;
public String sessionSecret;
}

/** Configuration options for the registry tool. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ misc:
# The number of milliseconds it'll sleep before giving up is (2^n - 2) * 100.
transientFailureRetries: 12

# Secret key used to sign EPP session cookies (SESSION_INFO).
sessionSecret: "default-unsigned-session-secret-key-placeholder"

beam:
# The default region to run Apache Beam (Cloud Dataflow) jobs in.
defaultJobRegion: us-central1
Expand Down
98 changes: 76 additions & 22 deletions core/src/main/java/google/registry/flows/CookieSessionMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package google.registry.flows;

import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
Expand All @@ -23,12 +24,15 @@
import com.google.common.io.BaseEncoding;
import google.registry.request.Response;
import jakarta.servlet.http.HttpServletRequest;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
* A metadata class that saves the data directly in cookies.
Expand Down Expand Up @@ -58,34 +62,43 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();

private final Map<String, String> data = new HashMap<>();
private final String sessionSecret;

public CookieSessionMetadata(HttpServletRequest request) {
public CookieSessionMetadata(HttpServletRequest request, String sessionSecret) {
this.sessionSecret = sessionSecret;
Optional.ofNullable(request.getHeader("Cookie"))
.ifPresent(
cookie -> {
Matcher matcher = COOKIE_PATTERN.matcher(cookie);
if (matcher.find()) {
String sessionInfo = decode(matcher.group(1));
logger.atInfo().log("SESSION INFO: %s", sessionInfo);
matcher = REGISTRAR_ID_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String registrarId = matcher.group(1);
if (!registrarId.equals("null")) {
data.put(REGISTRAR_ID, registrarId);
String cookieValue = matcher.group(1);
if (cookieValue != null) {
Optional<String> sessionInfoOpt =
verifyAndDecodeCookie(cookieValue, sessionSecret);
if (sessionInfoOpt.isPresent()) {
String sessionInfo = sessionInfoOpt.get();
logger.atInfo().log("SESSION INFO: %s", sessionInfo);

Check warning

Code scanning / CodeQL

Log Injection Medium

This log entry depends on a
user-provided value
.
Matcher matcher2 = REGISTRAR_ID_PATTERN.matcher(sessionInfo);
if (matcher2.find()) {
String registrarId = matcher2.group(1);
if (!registrarId.equals("null")) {
data.put(REGISTRAR_ID, registrarId);
}
}
matcher2 = SERVICE_EXTENSIONS_PATTERN.matcher(sessionInfo);
if (matcher2.find()) {
String serviceExtensions = matcher2.group(1);
if (serviceExtensions != null) {
data.put(SERVICE_EXTENSIONS, serviceExtensions);
}
}
matcher2 = FAILED_LOGIN_ATTEMPTS_PATTERN.matcher(sessionInfo);
if (matcher2.find()) {
String failedLoginAttempts = matcher2.group(1);
data.put(FAILED_LOGIN_ATTEMPTS, failedLoginAttempts);
}
}
}
matcher = SERVICE_EXTENSIONS_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String serviceExtensions = matcher.group(1);
if (serviceExtensions != null) {
data.put(SERVICE_EXTENSIONS, serviceExtensions);
}
}
matcher = FAILED_LOGIN_ATTEMPTS_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String failedLoginAttempts = matcher.group(1);
data.put(FAILED_LOGIN_ATTEMPTS, failedLoginAttempts);
}
}
});
}
Expand Down Expand Up @@ -141,8 +154,49 @@

@Override
public void save(Response response) {
String value = encode(toString());
response.setHeader("Set-Cookie", COOKIE_NAME + "=" + value);
String payload = toString();
try {
byte[] payloadBytes = payload.getBytes(US_ASCII);
byte[] signatureBytes = calculateHmac(payloadBytes, sessionSecret);
String encodedPayload = BaseEncoding.base64Url().encode(payloadBytes);
String encodedSignature = BaseEncoding.base64Url().encode(signatureBytes);
String cookieValue = encodedPayload + "." + encodedSignature;
response.setHeader("Set-Cookie", COOKIE_NAME + "=" + cookieValue);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to sign and save session cookie.");
}
}

private static Optional<String> verifyAndDecodeCookie(String cookieValue, String secret) {
int dotIndex = cookieValue.indexOf('.');
if (dotIndex == -1) {
logger.atWarning().log("Cookie does not contain a signature.");
return Optional.empty();
}
String encodedPayload = cookieValue.substring(0, dotIndex);
String encodedSignature = cookieValue.substring(dotIndex + 1);
try {
byte[] payloadBytes = BaseEncoding.base64Url().decode(encodedPayload);
byte[] signatureBytes = BaseEncoding.base64Url().decode(encodedSignature);
byte[] expectedSignatureBytes = calculateHmac(payloadBytes, secret);
if (MessageDigest.isEqual(signatureBytes, expectedSignatureBytes)) {
return Optional.of(new String(payloadBytes, US_ASCII));
} else {
logger.atWarning().log("Cookie signature verification failed.");
}
} catch (IllegalArgumentException e) {
logger.atWarning().withCause(e).log("Failed to decode cookie payload/signature.");
} catch (Exception e) {
logger.atWarning().withCause(e).log("Error verifying cookie signature.");
}
return Optional.empty();
}

private static byte[] calculateHmac(byte[] data, String secret) throws Exception {
SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
return mac.doFinal(data);
}

protected static String encode(String plainText) {
Expand Down
8 changes: 7 additions & 1 deletion core/src/main/java/google/registry/flows/EppTlsAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package google.registry.flows;

import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action;
import google.registry.request.Action.Method;
import google.registry.request.Payload;
Expand All @@ -36,12 +37,17 @@ public class EppTlsAction implements Runnable {
@Inject TlsCredentials tlsCredentials;
@Inject HttpServletRequest request;
@Inject EppRequestHandler eppRequestHandler;

@Inject
@Config("sessionSecret")
String sessionSecret;

@Inject EppTlsAction() {}

@Override
public void run() {
eppRequestHandler.executeEpp(
new CookieSessionMetadata(request),
new CookieSessionMetadata(request, sessionSecret),
tlsCredentials,
EppRequestSource.TLS,
false, // This endpoint is never a dry run.
Expand Down
Loading
Loading