From 7c23413d8304e37626aba742f994faf25125078d Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Wed, 24 Jun 2026 16:39:42 -0400 Subject: [PATCH] Fix Console API and Angular XSS security flaws (#3076) This commit addresses the following security vulnerabilities identified in the recent audit of the Console App and Backend APIs: 1. Angular XSS: Removed unsafe [innerHTML] bindings across all console-webapp templates (Contact, Registrars, Registrar Details, Users List) in favor of standard Angular interpolation. 2. Broken Access Control (IDOR): PasswordResetRequestAction and PasswordResetVerifyAction now explicitly verify that the target user's email belongs to the authorized registrarId. 3. Missing Permission Check: ConsoleEppPasswordAction now explicitly checks for CONFIGURE_EPP_CONNECTION permission before updating the EPP password. 4. Denial of Service (DoS): ConsoleBulkDomainAction now strictly limits the size of bulk domain lists (configurable, default 500) to prevent thread exhaustion. 5. Denial of Service (OOM): ConsoleHistoryDataAction now uses .setMaxResults() (configurable, default 500) on JPA native queries to prevent eager loading of the entire database into memory. Makes the history query limit and bulk domain action limit configurable via RegistryConfig, allowing smaller limits to be used in tests to avoid heavy resource persistence. Also removes an outdated Joda-Time migration reference from GEMINI.md. --- GEMINI.md | 2 +- .../src/app/domains/domainList.component.ts | 18 ++++++--- .../registrar/registrarDetails.component.html | 7 ++-- .../registrar/registrarsTable.component.html | 7 ++-- .../registrar/registrarsTable.component.ts | 2 +- .../settings/contact/contact.component.html | 13 ++++++- .../app/settings/contact/contact.component.ts | 9 +---- .../src/app/users/usersList.component.html | 2 +- .../registry/config/RegistryConfig.java | 12 ++++++ .../config/RegistryConfigSettings.java | 2 + .../registry/config/files/default-config.yaml | 6 +++ .../console/ConsoleEppPasswordAction.java | 3 ++ .../console/ConsoleHistoryDataAction.java | 4 ++ .../console/PasswordResetRequestAction.java | 26 +++++++++---- .../console/PasswordResetVerifyAction.java | 4 +- .../domains/ConsoleBulkDomainAction.java | 12 ++++++ .../console/ConsoleHistoryDataActionTest.java | 30 ++++++++++++++- .../domains/ConsoleBulkDomainActionTest.java | 37 +++++++++++++++++-- 18 files changed, 158 insertions(+), 38 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index b393c0773..ab00dcdb1 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -94,7 +94,7 @@ Do not wait for the user to tell you to improve the skills; it is your responsib # Gemini Engineering Guide: Nomulus Codebase -This document captures high-level architectural patterns, lessons learned from large-scale refactorings (like the Joda-Time to `java.time` migration), and specific instructions to avoid common pitfalls in this environment. +This document captures high-level architectural patterns, lessons learned from large-scale refactorings, and specific instructions to avoid common pitfalls in this environment. ## 🏛 Architecture Overview diff --git a/console-webapp/src/app/domains/domainList.component.ts b/console-webapp/src/app/domains/domainList.component.ts index 0cc3e8ccf..21327f1df 100644 --- a/console-webapp/src/app/domains/domainList.component.ts +++ b/console-webapp/src/app/domains/domainList.component.ts @@ -48,7 +48,11 @@ interface DomainData { selector: 'app-response-dialog', template: `

{{ data.title }}

- + + @for (line of data.content; track line) { +
{{ line }}
+ } +
@@ -59,7 +63,7 @@ export class ResponseDialogComponent { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) - public data: { title: string; content: string } + public data: { title: string; content: string[] } ) {} onClose(): void { @@ -312,11 +316,13 @@ export class DomainListComponent { this.dialog.open(ResponseDialogComponent, { data: { title: 'Domain Deletion Results', - content: `Successfully deleted - ${successCount} domain(s)
Failed to delete - ${failureCount} domain(s)
${ + content: [ + `Successfully deleted - ${successCount} domain(s)`, + `Failed to delete - ${failureCount} domain(s)`, failureCount - ? 'Some domains could not be deleted due to ongoing processes or server errors. ' - : '' - }Please check the table for more information.`, + ? 'Some domains could not be deleted due to ongoing processes or server errors. Please check the table for more information.' + : 'Please check the table for more information.', + ], }, }); this.selection.clear(); diff --git a/console-webapp/src/app/registrar/registrarDetails.component.html b/console-webapp/src/app/registrar/registrarDetails.component.html index 1eda62f33..38937c15e 100644 --- a/console-webapp/src/app/registrar/registrarDetails.component.html +++ b/console-webapp/src/app/registrar/registrarDetails.component.html @@ -97,10 +97,9 @@ @for (column of columns; track column.columnDef) { {{ column.header }} - + {{ + column.cell(registrarInEdit) + }} } diff --git a/console-webapp/src/app/registrar/registrarsTable.component.html b/console-webapp/src/app/registrar/registrarsTable.component.html index d2878b7f2..5ec5f42da 100644 --- a/console-webapp/src/app/registrar/registrarsTable.component.html +++ b/console-webapp/src/app/registrar/registrarsTable.component.html @@ -49,10 +49,9 @@ {{ column.header }} - + {{ + column.cell(row) + }} } diff --git a/console-webapp/src/app/registrar/registrarsTable.component.ts b/console-webapp/src/app/registrar/registrarsTable.component.ts index 702a61bcd..69f892a3a 100644 --- a/console-webapp/src/app/registrar/registrarsTable.component.ts +++ b/console-webapp/src/app/registrar/registrarsTable.component.ts @@ -56,7 +56,7 @@ export const columns = [ cell: (record: Registrar) => `${Object.entries(record.billingAccountMap || {}).reduce( (acc, [key, val]) => { - return `${acc}${key}=${val}
`; + return `${acc}${key}=${val}\n`; }, '' )}`, diff --git a/console-webapp/src/app/settings/contact/contact.component.html b/console-webapp/src/app/settings/contact/contact.component.html index 362c8396c..4b3528ab6 100644 --- a/console-webapp/src/app/settings/contact/contact.component.html +++ b/console-webapp/src/app/settings/contact/contact.component.html @@ -25,7 +25,18 @@ @for (column of columns; track column) { {{ column.header }} - + + @if (column.columnDef === 'name') { +
+
{{ row.name }}
+
+ {{ row.userFriendlyTypes.join(" • ") }} +
+
+ } @else { + {{ column.cell(row) }} + } +
} diff --git a/console-webapp/src/app/settings/contact/contact.component.ts b/console-webapp/src/app/settings/contact/contact.component.ts index cc23a7c3b..c9dd0c2f3 100644 --- a/console-webapp/src/app/settings/contact/contact.component.ts +++ b/console-webapp/src/app/settings/contact/contact.component.ts @@ -34,14 +34,7 @@ export default class ContactComponent { { columnDef: 'name', header: 'Name', - cell: (contact: ViewReadyContact) => ` -
-
${contact.name}
-
${contact.userFriendlyTypes.join( - ' • ' - )}
-
- `, + cell: (contact: ViewReadyContact) => `${contact.name}`, }, { columnDef: 'emailAddress', diff --git a/console-webapp/src/app/users/usersList.component.html b/console-webapp/src/app/users/usersList.component.html index eb88a02b3..8cc7b2a0c 100644 --- a/console-webapp/src/app/users/usersList.component.html +++ b/console-webapp/src/app/users/usersList.component.html @@ -10,7 +10,7 @@ {{ column.header }} - + {{ column.cell(row) }} } diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 7c7b9448d..cee883033 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -173,6 +173,18 @@ public final class RegistryConfig { return config.registrarConsole.supportEmailAddress; } + @Provides + @Config("consoleHistoryQueryLimit") + public static int provideConsoleHistoryQueryLimit(RegistryConfigSettings config) { + return config.registrarConsole.historyQueryLimit; + } + + @Provides + @Config("consoleBulkDomainActionLimit") + public static int provideConsoleBulkDomainActionLimit(RegistryConfigSettings config) { + return config.registrarConsole.bulkDomainActionLimit; + } + /** * The DUM file name, used as a file name base for DUM csv file * diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index 1f2331b37..5b8ddedad 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -181,6 +181,8 @@ public class RegistryConfigSettings { public String supportPhoneNumber; public String supportEmailAddress; public String technicalDocsUrl; + public int historyQueryLimit; + public int bulkDomainActionLimit; } /** Configuration for monitoring. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index 22f751de1..9b8719d75 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -380,6 +380,12 @@ registrarConsole: # URL linking to directory of technical support docs on the registry. technicalDocsUrl: http://example.com/your_support_docs/ + # Maximum number of history records returned in a single query. + historyQueryLimit: 500 + + # Maximum number of domains allowed in a single bulk action. + bulkDomainActionLimit: 500 + monitoring: # Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver) # API. The limit can be adjusted by contacting Cloud Support. diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java index a9c894331..a7384e361 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleEppPasswordAction.java @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet; import com.google.gson.annotations.Expose; import google.registry.flows.EppException.AuthenticationErrorException; import google.registry.flows.PasswordOnlyTransportCredentials; +import google.registry.model.console.ConsolePermission; import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.User; import google.registry.model.registrar.Registrar; @@ -84,6 +85,8 @@ public class ConsoleEppPasswordAction extends ConsoleApiAction { eppRequestBody.newPassword().equals(eppRequestBody.newPasswordRepeat()), "New password fields don't match"); + checkPermission(user, eppRequestBody.registrarId(), ConsolePermission.CONFIGURE_EPP_CONNECTION); + Registrar registrar; try { registrar = registrarAccessor.getRegistrar(eppRequestBody.registrarId()); diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java index d967e1e3e..138e1a1f4 100644 --- a/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleHistoryDataAction.java @@ -62,17 +62,20 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction { private final Optional consoleUserEmail; private final String supportEmail; + private final int historyQueryLimit; @Inject public ConsoleHistoryDataAction( ConsoleApiParams consoleApiParams, @Config("supportEmail") String supportEmail, + @Config("consoleHistoryQueryLimit") int historyQueryLimit, @Parameter("registrarId") String registrarId, @Parameter("consoleUserEmail") Optional consoleUserEmail) { super(consoleApiParams); this.registrarId = registrarId; this.consoleUserEmail = consoleUserEmail; this.supportEmail = supportEmail; + this.historyQueryLimit = historyQueryLimit; } @Override @@ -117,6 +120,7 @@ public class ConsoleHistoryDataAction extends ConsoleApiAction { .createNativeQuery(SQL_REGISTRAR_HISTORY, ConsoleUpdateHistory.class) .setParameter("registrarId", registrarId) .setHint("org.hibernate.fetchSize", 1000) + .setMaxResults(historyQueryLimit) .getResultList()); List formattedHistoryList = diff --git a/core/src/main/java/google/registry/ui/server/console/PasswordResetRequestAction.java b/core/src/main/java/google/registry/ui/server/console/PasswordResetRequestAction.java index 732e97f5a..899b2aa69 100644 --- a/core/src/main/java/google/registry/ui/server/console/PasswordResetRequestAction.java +++ b/core/src/main/java/google/registry/ui/server/console/PasswordResetRequestAction.java @@ -86,7 +86,7 @@ public class PasswordResetRequestAction extends ConsoleApiAction { "Must provide registry lock email to reset"); requiredPermission = ConsolePermission.MANAGE_USERS; destinationEmail = passwordResetRequestData.registryLockEmail; - checkUserExistsWithRegistryLockEmail(destinationEmail); + checkUserExistsWithRegistryLockEmail(destinationEmail, registrarId); emailSubject = "Registry lock password reset request"; } default -> throw new IllegalArgumentException("Unknown type " + type); @@ -121,12 +121,24 @@ public class PasswordResetRequestAction extends ConsoleApiAction { .sendEmail(EmailMessage.create(emailSubject, body, destinationAddress)); } - static User checkUserExistsWithRegistryLockEmail(String destinationEmail) { - return tm().createQueryComposer(User.class) - .where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail) - .first() - .orElseThrow( - () -> new IllegalArgumentException("Unknown user with lock email " + destinationEmail)); + static User checkUserExistsWithRegistryLockEmail(String destinationEmail, String registrarId) { + User targetUser = + tm().createQueryComposer(User.class) + .where("registryLockEmailAddress", QueryComposer.Comparator.EQ, destinationEmail) + .first() + .orElseThrow( + () -> + new IllegalArgumentException( + "Unknown user with lock email " + destinationEmail)); + + // Prevent IDOR: Ensure the resolved user actually belongs to the registrar the requester + // has permissions for, or is a global admin. + if (!targetUser.getUserRoles().isAdmin() + && !targetUser.getUserRoles().getRegistrarRoles().containsKey(registrarId)) { + throw new IllegalArgumentException( + "User with lock email " + destinationEmail + " is not associated with " + registrarId); + } + return targetUser; } private String getAdminPocEmail(String registrarId) { diff --git a/core/src/main/java/google/registry/ui/server/console/PasswordResetVerifyAction.java b/core/src/main/java/google/registry/ui/server/console/PasswordResetVerifyAction.java index 796c4512f..52c2cc889 100644 --- a/core/src/main/java/google/registry/ui/server/console/PasswordResetVerifyAction.java +++ b/core/src/main/java/google/registry/ui/server/console/PasswordResetVerifyAction.java @@ -98,7 +98,9 @@ public class PasswordResetVerifyAction extends ConsoleApiAction { } private void handleRegistryLockPasswordReset(PasswordResetRequest request) { - User affectedUser = checkUserExistsWithRegistryLockEmail(request.getDestinationEmail()); + User affectedUser = + checkUserExistsWithRegistryLockEmail( + request.getDestinationEmail(), request.getRegistrarId()); tm().put( affectedUser .asBuilder() diff --git a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java index b4ed47db1..7de63e199 100644 --- a/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java +++ b/core/src/main/java/google/registry/ui/server/console/domains/ConsoleBulkDomainAction.java @@ -14,6 +14,7 @@ package google.registry.ui.server.console.domains; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static jakarta.servlet.http.HttpServletResponse.SC_OK; @@ -22,6 +23,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; import com.google.gson.annotations.Expose; +import google.registry.config.RegistryConfig.Config; import google.registry.flows.EppController; import google.registry.flows.EppRequestSource; import google.registry.flows.PasswordOnlyTransportCredentials; @@ -64,11 +66,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction { private final String registrarId; private final String bulkDomainAction; private final Optional optionalJsonPayload; + private final int bulkDomainActionLimit; @Inject public ConsoleBulkDomainAction( ConsoleApiParams consoleApiParams, EppController eppController, + @Config("consoleBulkDomainActionLimit") int bulkDomainActionLimit, @Parameter("registrarId") String registrarId, @Parameter("bulkDomainAction") String bulkDomainAction, @OptionalJsonPayload Optional optionalJsonPayload) { @@ -77,6 +81,7 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction { this.registrarId = registrarId; this.bulkDomainAction = bulkDomainAction; this.optionalJsonPayload = optionalJsonPayload; + this.bulkDomainActionLimit = bulkDomainActionLimit; } @Override @@ -85,6 +90,13 @@ public class ConsoleBulkDomainAction extends ConsoleApiAction { optionalJsonPayload.orElseThrow( () -> new IllegalArgumentException("Bulk action payload must be present")); BulkDomainList domainList = consoleApiParams.gson().fromJson(jsonPayload, BulkDomainList.class); + checkArgument( + domainList.domainList != null && !domainList.domainList.isEmpty(), + "Domain list cannot be empty"); + checkArgument( + domainList.domainList.size() <= bulkDomainActionLimit, + "Cannot process more than %s domains in a single bulk action", + bulkDomainActionLimit); ConsoleDomainActionType actionType = ConsoleDomainActionType.parseActionType(bulkDomainAction, jsonPayload); diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java index d7fee4abc..137d31572 100644 --- a/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleHistoryDataActionTest.java @@ -23,6 +23,8 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; import google.registry.model.console.ConsoleUpdateHistory; import google.registry.model.console.RegistrarRole; import google.registry.model.console.User; @@ -162,6 +164,27 @@ class ConsoleHistoryDataActionTest extends ConsoleActionBaseTestCase { assertThat(response.getPayload()).isEqualTo("[]"); } + @Test + void testSuccess_limitsResults() { + for (int i = 0; i < 10; i++) { + DatabaseHelper.persistResource( + new ConsoleUpdateHistory.Builder() + .setType(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE) + .setDescription("TheRegistrar|some detail " + i) + .setActingUser(fteUser) + .setUrl("https://test.com") + .setMethod("GET") + .setModificationTime(clock.now()) + .build()); + } + ConsoleHistoryDataAction action = + createAction(AuthResult.createUser(fteUser), "TheRegistrar", Optional.empty(), 5); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_OK); + JsonArray payload = JsonParser.parseString(response.getPayload()).getAsJsonArray(); + assertThat(payload.size()).isEqualTo(5); + } + @Test void testFailure_getByRegistrar_noPermission() { ConsoleHistoryDataAction action = @@ -192,10 +215,15 @@ class ConsoleHistoryDataActionTest extends ConsoleActionBaseTestCase { private ConsoleHistoryDataAction createAction( AuthResult authResult, String registrarId, Optional consoleUserEmail) { + return createAction(authResult, registrarId, consoleUserEmail, 500); + } + + private ConsoleHistoryDataAction createAction( + AuthResult authResult, String registrarId, Optional consoleUserEmail, int limit) { consoleApiParams = ConsoleApiParamsUtils.createFake(authResult); when(consoleApiParams.request().getMethod()).thenReturn("GET"); response = (FakeResponse) consoleApiParams.response(); return new ConsoleHistoryDataAction( - consoleApiParams, SUPPORT_EMAIL, registrarId, consoleUserEmail); + consoleApiParams, SUPPORT_EMAIL, limit, registrarId, consoleUserEmail); } } diff --git a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java index 84afa9956..ed83fd741 100644 --- a/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java +++ b/core/src/test/java/google/registry/ui/server/console/domains/ConsoleBulkDomainActionTest.java @@ -45,6 +45,7 @@ import google.registry.testing.ConsoleApiParamsUtils; import google.registry.testing.FakeResponse; import google.registry.ui.server.console.ConsoleActionBaseTestCase; import google.registry.ui.server.console.ConsoleApiParams; +import java.util.Collections; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -173,7 +174,9 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase { @Test void testFailure_badActionString() { - ConsoleBulkDomainAction action = createAction("bad", GSON.toJsonTree(ImmutableMap.of())); + ConsoleBulkDomainAction action = + createAction( + "bad", GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of("domain.tld")))); action.run(); assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); assertThat(response.getPayload()) @@ -190,6 +193,29 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase { assertThat(response.getPayload()).isEqualTo("Bulk action payload must be present"); } + @Test + void testFailure_listTooLarge() { + JsonElement payload = + GSON.toJsonTree( + ImmutableMap.of( + "domainList", Collections.nCopies(6, "domain.tld"), "reason", "reason")); + ConsoleBulkDomainAction action = createAction("DELETE", payload, fteUser, 5); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()) + .isEqualTo("Cannot process more than 5 domains in a single bulk action"); + } + + @Test + void testFailure_emptyList() { + JsonElement payload = + GSON.toJsonTree(ImmutableMap.of("domainList", ImmutableList.of(), "reason", "reason")); + ConsoleBulkDomainAction action = createAction("DELETE", payload); + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST); + assertThat(response.getPayload()).isEqualTo("Domain list cannot be empty"); + } + @Test void testFailure_noPermission() { JsonElement payload = @@ -232,15 +258,20 @@ public class ConsoleBulkDomainActionTest extends ConsoleActionBaseTestCase { // } private ConsoleBulkDomainAction createAction(String action, JsonElement payload) { - return createAction(action, payload, fteUser); + return createAction(action, payload, fteUser, 500); } private ConsoleBulkDomainAction createAction(String action, JsonElement payload, User user) { + return createAction(action, payload, user, 500); + } + + private ConsoleBulkDomainAction createAction( + String action, JsonElement payload, User user, int limit) { AuthResult authResult = AuthResult.createUser(user); ConsoleApiParams params = ConsoleApiParamsUtils.createFake(authResult); when(params.request().getMethod()).thenReturn("POST"); response = (FakeResponse) params.response(); return new ConsoleBulkDomainAction( - params, eppController, "TheRegistrar", action, Optional.ofNullable(payload)); + params, eppController, limit, "TheRegistrar", action, Optional.ofNullable(payload)); } }