mirror of
https://github.com/google/nomulus
synced 2026-07-26 01:53:16 +00:00
Forbid subordinate host changes when superord domain is locked (#3171)
This is not explicitly called out that I could find in the RFCs, however it is very clear from the RFCs that hosts are subordinate to their domains and it stands to reason that updates to a subordinate host are updates to the superordinate domain. Section 1.1 of RFC 5732 specifies: ``` host name "ns1.example.com" has a subordinate relationship to domain name "example.com". EPP actions (such as object transfers) that do not preserve this relationship MUST be explicitly disallowed. ``` Allowing host updates (e.g. renames) opens up situations where this relationship could be severed. We add analogous prohibitions on deletion and creation of subordinate hosts as well. b/534930957
This commit is contained in:
@@ -20,6 +20,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceDoesNotExist
|
||||
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
|
||||
import static google.registry.flows.host.HostFlowUtils.validateHostName;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
|
||||
import static google.registry.model.EppResourceUtils.createRepoId;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.HOST_CREATE;
|
||||
@@ -73,6 +74,7 @@ import java.util.Optional;
|
||||
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
|
||||
* @error {@link SubordinateHostMustHaveIpException}
|
||||
* @error {@link UnexpectedExternalHostIpException}
|
||||
*/
|
||||
@@ -107,6 +109,7 @@ public final class HostCreateFlow implements MutatingFlow {
|
||||
Optional<Domain> superordinateDomain =
|
||||
lookupSuperordinateDomain(validateHostName(targetId), now);
|
||||
verifySuperordinateDomainNotInPendingDelete(superordinateDomain.orElse(null));
|
||||
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain.orElse(null));
|
||||
verifySuperordinateDomainOwnership(registrarId, superordinateDomain.orElse(null));
|
||||
boolean willBeSubordinate = superordinateDomain.isPresent();
|
||||
boolean hasIpAddresses = !isNullOrEmpty(command.getInetAddresses());
|
||||
|
||||
@@ -22,6 +22,7 @@ import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
|
||||
import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses;
|
||||
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.host.HostFlowUtils.validateHostName;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
|
||||
import static google.registry.model.eppoutput.Result.Code.SUCCESS;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
@@ -33,7 +34,7 @@ import google.registry.flows.FlowModule.Superuser;
|
||||
import google.registry.flows.FlowModule.TargetId;
|
||||
import google.registry.flows.MutatingFlow;
|
||||
import google.registry.flows.annotations.ReportingSpec;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.metadata.MetadataExtension;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.eppcommon.Trid;
|
||||
@@ -62,6 +63,7 @@ import java.time.Instant;
|
||||
* @error {@link HostFlowUtils.HostNameNotLowerCaseException}
|
||||
* @error {@link HostFlowUtils.HostNameNotNormalizedException}
|
||||
* @error {@link HostFlowUtils.HostNameNotPunyCodedException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
|
||||
*/
|
||||
@ReportingSpec(ActivityReportField.HOST_DELETE)
|
||||
public final class HostDeleteFlow implements MutatingFlow {
|
||||
@@ -90,12 +92,15 @@ public final class HostDeleteFlow implements MutatingFlow {
|
||||
if (!isSuperuser) {
|
||||
verifyNoDisallowedStatuses(existingHost, DELETE_PROHIBITED_STATUSES);
|
||||
// Hosts transfer with their superordinate domains, so for hosts with a superordinate domain,
|
||||
// the client id, needs to be read off of it.
|
||||
EppResource owningResource =
|
||||
existingHost.isSubordinate()
|
||||
? tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now)
|
||||
: existingHost;
|
||||
verifyResourceOwnership(registrarId, owningResource);
|
||||
// the client id needs to be read off of it.
|
||||
if (existingHost.isSubordinate()) {
|
||||
Domain superordinateDomain =
|
||||
tm().loadByKey(existingHost.getSuperordinateDomain()).cloneProjectedAtTime(now);
|
||||
verifyResourceOwnership(registrarId, superordinateDomain);
|
||||
verifySuperordinateDomainNotServerUpdateProhibited(superordinateDomain);
|
||||
} else {
|
||||
verifyResourceOwnership(registrarId, existingHost);
|
||||
}
|
||||
}
|
||||
Host newHost = existingHost.asBuilder().setStatusValues(null).setDeletionTime(now).build();
|
||||
if (existingHost.isSubordinate()) {
|
||||
|
||||
@@ -174,6 +174,31 @@ public class HostFlowUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the superordinate domain does not have {@code serverUpdateProhibited} status.
|
||||
*
|
||||
* <p>Subordinate hosts publish glue A/AAAA records in the TLD zone on behalf of their
|
||||
* superordinate domain. If the superordinate domain is registry-locked (carries {@link
|
||||
* StatusValue#SERVER_UPDATE_PROHIBITED}), non-superuser modifications to its subordinate hosts
|
||||
* must be rejected as well; otherwise a compromised sponsoring registrar could hijack DNS for a
|
||||
* locked domain by swapping the glue addresses of its in-bailiwick nameservers.
|
||||
*/
|
||||
static void verifySuperordinateDomainNotServerUpdateProhibited(Domain superordinateDomain)
|
||||
throws EppException {
|
||||
if ((superordinateDomain != null)
|
||||
&& superordinateDomain.getStatusValues().contains(StatusValue.SERVER_UPDATE_PROHIBITED)) {
|
||||
throw new SuperordinateDomainServerUpdateProhibitedException();
|
||||
}
|
||||
}
|
||||
|
||||
/** Superordinate domain for this hostname has serverUpdateProhibited status. */
|
||||
static class SuperordinateDomainServerUpdateProhibitedException
|
||||
extends StatusProhibitsOperationException {
|
||||
public SuperordinateDomainServerUpdateProhibitedException() {
|
||||
super("Superordinate domain for this hostname has serverUpdateProhibited status");
|
||||
}
|
||||
}
|
||||
|
||||
/** Host names are limited to 253 characters. */
|
||||
static class HostNameTooLongException extends ParameterValueRangeErrorException {
|
||||
public HostNameTooLongException() {
|
||||
|
||||
@@ -28,6 +28,7 @@ import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
|
||||
import static google.registry.flows.host.HostFlowUtils.lookupSuperordinateDomain;
|
||||
import static google.registry.flows.host.HostFlowUtils.validateHostName;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotInPendingDelete;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainNotServerUpdateProhibited;
|
||||
import static google.registry.flows.host.HostFlowUtils.verifySuperordinateDomainOwnership;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.HOST_UPDATE;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
@@ -98,6 +99,7 @@ import java.util.Optional;
|
||||
* @error {@link HostFlowUtils.InvalidHostNameException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainInPendingDeleteException}
|
||||
* @error {@link HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException}
|
||||
* @error {@link CannotAddIpToExternalHostException}
|
||||
* @error {@link CannotRemoveSubordinateHostLastIpException}
|
||||
* @error {@link CannotRenameExternalHostException}
|
||||
@@ -152,7 +154,12 @@ public final class HostUpdateFlow implements MutatingFlow {
|
||||
verifySuperordinateDomainNotInPendingDelete(newSuperordinateDomain.orElse(null));
|
||||
EppResource owningResource = firstNonNull(oldSuperordinateDomain, existingHost);
|
||||
verifyUpdateAllowed(
|
||||
command, existingHost, newSuperordinateDomain.orElse(null), owningResource, isHostRename);
|
||||
command,
|
||||
existingHost,
|
||||
oldSuperordinateDomain,
|
||||
newSuperordinateDomain.orElse(null),
|
||||
owningResource,
|
||||
isHostRename);
|
||||
if (isHostRename && ForeignKeyUtils.loadKey(Host.class, newHostName, now).isPresent()) {
|
||||
throw new HostAlreadyExistsException(newHostName);
|
||||
}
|
||||
@@ -211,30 +218,38 @@ public final class HostUpdateFlow implements MutatingFlow {
|
||||
private void verifyUpdateAllowed(
|
||||
Update command,
|
||||
Host existingHost,
|
||||
Domain oldSuperordinateDomain,
|
||||
Domain newSuperordinateDomain,
|
||||
EppResource owningResource,
|
||||
boolean isHostRename)
|
||||
throws EppException {
|
||||
if (!isSuperuser) {
|
||||
// Verify that the host belongs to this registrar, either directly or because it is currently
|
||||
// subordinate to a domain owned by this registrar.
|
||||
verifyResourceOwnership(registrarId, owningResource);
|
||||
if (isHostRename && !existingHost.isSubordinate()) {
|
||||
throw new CannotRenameExternalHostException();
|
||||
}
|
||||
// Verify that the new superordinate domain belongs to this registrar.
|
||||
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
|
||||
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
|
||||
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
|
||||
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
|
||||
// status, then the update must be disallowed.
|
||||
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
|
||||
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
|
||||
throw new ResourceHasClientUpdateProhibitedException();
|
||||
}
|
||||
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
|
||||
}
|
||||
verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES);
|
||||
if (isSuperuser) {
|
||||
return;
|
||||
}
|
||||
// Verify that the host belongs to this registrar, either directly or because it is currently
|
||||
// subordinate to a domain owned by this registrar.
|
||||
verifyResourceOwnership(registrarId, owningResource);
|
||||
if (isHostRename && !existingHost.isSubordinate()) {
|
||||
throw new CannotRenameExternalHostException();
|
||||
}
|
||||
// Verify that the new superordinate domain belongs to this registrar.
|
||||
verifySuperordinateDomainOwnership(registrarId, newSuperordinateDomain);
|
||||
// Subordinate hosts inherit the registry-lock protection of their superordinate domain: if
|
||||
// either the current or the target superordinate domain carries SERVER_UPDATE_PROHIBITED,
|
||||
// non-superuser updates (including glue IP changes and renames) must be rejected so that a
|
||||
// compromised sponsoring registrar cannot hijack DNS for a registry-locked domain.
|
||||
verifySuperordinateDomainNotServerUpdateProhibited(oldSuperordinateDomain);
|
||||
verifySuperordinateDomainNotServerUpdateProhibited(newSuperordinateDomain);
|
||||
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
|
||||
ImmutableSet<StatusValue> statusesToRemove = command.getInnerRemove().getStatusValues();
|
||||
// If the resource is marked with clientUpdateProhibited, and this update does not clear that
|
||||
// status, then the update must be disallowed.
|
||||
if (existingHost.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
|
||||
&& !statusesToRemove.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
|
||||
throw new ResourceHasClientUpdateProhibitedException();
|
||||
}
|
||||
verifyAllStatusesAreClientSettable(union(statusesToAdd, statusesToRemove));
|
||||
}
|
||||
|
||||
private static void verifyHasIpsIffIsExternal(Update command, Host existingHost, Host newHost)
|
||||
|
||||
@@ -51,6 +51,7 @@ import google.registry.flows.host.HostFlowUtils.InvalidHostNameException;
|
||||
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
@@ -413,6 +414,22 @@ class HostCreateFlowTest extends ResourceFlowTestCase<HostCreateFlow, Host> {
|
||||
.marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_superordinateDomainIsServerUpdateProhibited() {
|
||||
createTld("tld");
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
|
||||
.build());
|
||||
setEppHostCreateInputWithIps("ns1.example.tld");
|
||||
SuperordinateDomainServerUpdateProhibitedException thrown =
|
||||
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
runFlow();
|
||||
|
||||
@@ -42,6 +42,7 @@ import google.registry.flows.exceptions.ResourceToDeleteIsReferencedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException;
|
||||
import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.domain.GracePeriod;
|
||||
import google.registry.model.domain.rgp.GracePeriodStatus;
|
||||
@@ -360,6 +361,25 @@ class HostDeleteFlowTest extends ResourceFlowTestCase<HostDeleteFlow, Host> {
|
||||
assertAboutEppExceptions().that(thrown).marshalsToXml();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_superordinateDomainIsServerUpdateProhibited() {
|
||||
createTld("tld");
|
||||
Domain domain =
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
|
||||
.build());
|
||||
persistResource(
|
||||
newHost("ns1.example.tld").asBuilder().setSuperordinateDomain(domain.createVKey()).build());
|
||||
clock.advanceOneMilli();
|
||||
SuperordinateDomainServerUpdateProhibitedException thrown =
|
||||
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIcannActivityReportField_getsLogged() throws Exception {
|
||||
persistActiveHost("ns1.example.tld");
|
||||
|
||||
@@ -68,6 +68,7 @@ import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException;
|
||||
import google.registry.flows.host.HostFlowUtils.IpAddressNotRoutableException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainDoesNotExistException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainInPendingDeleteException;
|
||||
import google.registry.flows.host.HostFlowUtils.SuperordinateDomainServerUpdateProhibitedException;
|
||||
import google.registry.flows.host.HostUpdateFlow.CannotAddIpToExternalHostException;
|
||||
import google.registry.flows.host.HostUpdateFlow.CannotRemoveSubordinateHostLastIpException;
|
||||
import google.registry.flows.host.HostUpdateFlow.CannotRenameExternalHostException;
|
||||
@@ -889,6 +890,30 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
|
||||
.contains("Superordinate domain for this hostname is in pending delete");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_superordinateDomainIsServerUpdateProhibited() throws Exception {
|
||||
setEppHostUpdateInput(
|
||||
"ns1.example.tld",
|
||||
"ns2.example.tld",
|
||||
"<host:addr ip=\"v4\">192.0.2.22</host:addr>",
|
||||
"<host:addr ip=\"v6\">1080:0:0:0:8:800:200C:417A</host:addr>");
|
||||
createTld("tld");
|
||||
Domain domain =
|
||||
persistResource(
|
||||
DatabaseHelper.newDomain("example.tld")
|
||||
.asBuilder()
|
||||
.setSubordinateHosts(ImmutableSet.of(oldHostName()))
|
||||
.setStatusValues(ImmutableSet.of(StatusValue.SERVER_UPDATE_PROHIBITED))
|
||||
.build());
|
||||
persistActiveSubordinateHost(oldHostName(), domain);
|
||||
clock.advanceOneMilli();
|
||||
SuperordinateDomainServerUpdateProhibitedException thrown =
|
||||
assertThrows(SuperordinateDomainServerUpdateProhibitedException.class, this::runFlow);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.contains("Superordinate domain for this hostname has serverUpdateProhibited status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_neverExisted() throws Exception {
|
||||
ResourceDoesNotExistException thrown =
|
||||
|
||||
Reference in New Issue
Block a user