fix(route53): resolve false positive in dangling IP check (#9952)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
Pawan Gambhir
2026-03-17 16:32:48 +05:30
committed by GitHub
parent 451071d694
commit df680ef277
4 changed files with 130 additions and 13 deletions

View File

@@ -24,6 +24,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Bump `multipart` to 1.3.1 to fix [GHSA-p2m9-wcp5-6qw3](https://github.com/defnull/multipart/security/advisories/GHSA-p2m9-wcp5-6qw3) [(#10331)](https://github.com/prowler-cloud/prowler/pull/10331)
### 🐞 Fixed
- Route53 dangling IP check false positive when using `--region` flag [(#9952)](https://github.com/prowler-cloud/prowler/pull/9952)
---
## [5.20.0] (Prowler v5.20.0)

View File

@@ -12,19 +12,21 @@ class route53_dangling_ip_subdomain_takeover(Check):
def execute(self) -> Check_Report_AWS:
findings = []
# When --region is used, Route53 service gathers EIPs from all regions
# to avoid false positives. Otherwise, use ec2_client data directly.
if route53_client.all_account_elastic_ips:
public_ips = list(route53_client.all_account_elastic_ips)
else:
public_ips = [eip.public_ip for eip in ec2_client.elastic_ips]
# Add Network Interface public IPs from audited regions
for ni in ec2_client.network_interfaces.values():
if ni.association and ni.association.get("PublicIp"):
public_ips.append(ni.association.get("PublicIp"))
for record_set in route53_client.record_sets:
# Check only A records and avoid aliases (only need to check IPs not AWS Resources)
if record_set.type == "A" and not record_set.is_alias:
# Gather Elastic IPs and Network Interfaces Public IPs inside the AWS Account
public_ips = []
public_ips.extend([eip.public_ip for eip in ec2_client.elastic_ips])
# Add public IPs from Network Interfaces
for network_interface in ec2_client.network_interfaces.values():
if (
network_interface.association
and network_interface.association.get("PublicIp")
):
public_ips.append(network_interface.association.get("PublicIp"))
for record in record_set.records:
# Check if record is an IP Address
if validate_ip_address(record):

View File

@@ -13,10 +13,20 @@ class Route53(AWSService):
super().__init__(__class__.__name__, provider, global_service=True)
self.hosted_zones = {}
self.record_sets = []
self.all_account_elastic_ips = []
self._list_hosted_zones()
self._list_query_logging_configs()
self._list_tags_for_resource()
self._list_resource_record_sets()
# Gather Elastic IPs from all regions only when the --region flag is used,
# since EC2 service will only have EIPs from the specified region(s) but
# Route53 is global and can reference EIPs from any region.
if (
"route53_dangling_ip_subdomain_takeover"
in provider.audit_metadata.expected_checks
and provider._identity.audited_regions
):
self._get_all_region_elastic_ips()
def _list_hosted_zones(self):
logger.info("Route53 - Listing Hosting Zones...")
@@ -77,6 +87,31 @@ class Route53(AWSService):
f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_all_region_elastic_ips(self):
"""Gather Elastic IPs from all enabled regions since Route53 is a global service.
When running Prowler with --region, ec2_client.elastic_ips is scoped
to the specified region(s). Route53 records can reference EIPs from any
region, so we need to query all enabled regions to avoid false positives.
"""
logger.info("Route53 - Gathering Elastic IPs from all regions...")
all_regions = self.provider._enabled_regions or set(
self.provider._identity.audited_regions
)
for region in all_regions:
try:
regional_ec2_client = self.session.client("ec2", region_name=region)
for addr in regional_ec2_client.describe_addresses().get(
"Addresses", []
):
if "PublicIp" in addr:
self.all_account_elastic_ips.append(addr["PublicIp"])
except Exception as error:
logger.warning(
f"{region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _list_query_logging_configs(self):
logger.info("Route53 - Listing Query Logging Configs...")
try:

View File

@@ -3,7 +3,11 @@ from unittest import mock
from boto3 import client, resource
from moto import mock_aws
from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider
from tests.providers.aws.utils import (
AWS_REGION_US_EAST_1,
AWS_REGION_US_WEST_2,
set_mocked_aws_provider,
)
HOSTED_ZONE_NAME = "testdns.aws.com."
@@ -309,7 +313,10 @@ class Test_route53_dangling_ip_subdomain_takeover:
from prowler.providers.aws.services.ec2.ec2_service import EC2
from prowler.providers.aws.services.route53.route53_service import Route53
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1],
expected_checks=["route53_dangling_ip_subdomain_takeover"],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -387,7 +394,10 @@ class Test_route53_dangling_ip_subdomain_takeover:
from prowler.providers.aws.services.ec2.ec2_service import EC2
from prowler.providers.aws.services.route53.route53_service import Route53
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider = set_mocked_aws_provider(
[AWS_REGION_US_EAST_1],
expected_checks=["route53_dangling_ip_subdomain_takeover"],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -426,3 +436,69 @@ class Test_route53_dangling_ip_subdomain_takeover:
result[0].resource_arn
== f"arn:{aws_provider.identity.partition}:route53:::hostedzone/{zone_id.replace('/hostedzone/', '')}"
)
@mock_aws
def test_hosted_zone_eip_cross_region(self):
"""EIP in us-west-2 referenced by Route53 A record should PASS even when auditing us-east-1 only."""
conn = client("route53", region_name=AWS_REGION_US_EAST_1)
ec2_west = client("ec2", region_name=AWS_REGION_US_WEST_2)
address = "17.5.7.3"
ec2_west.allocate_address(Domain="vpc", Address=address)
zone_id = conn.create_hosted_zone(
Name=HOSTED_ZONE_NAME, CallerReference=str(hash("foo"))
)["HostedZone"]["Id"]
record_set_name = "foo.bar.testdns.aws.com."
record_ip = address
conn.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch={
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": record_set_name,
"Type": "A",
"ResourceRecords": [{"Value": record_ip}],
},
}
]
},
)
from prowler.providers.aws.services.ec2.ec2_service import EC2
from prowler.providers.aws.services.route53.route53_service import Route53
# Audit only us-east-1 but enable both regions so Route53 finds the cross-region EIP
aws_provider = set_mocked_aws_provider(
audited_regions=[AWS_REGION_US_EAST_1],
enabled_regions={AWS_REGION_US_EAST_1, AWS_REGION_US_WEST_2},
expected_checks=["route53_dangling_ip_subdomain_takeover"],
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
):
with mock.patch(
"prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.route53_client",
new=Route53(aws_provider),
):
with mock.patch(
"prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover.ec2_client",
new=EC2(aws_provider),
):
from prowler.providers.aws.services.route53.route53_dangling_ip_subdomain_takeover.route53_dangling_ip_subdomain_takeover import (
route53_dangling_ip_subdomain_takeover,
)
check = route53_dangling_ip_subdomain_takeover()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Route53 record {record_ip} (name: {record_set_name}) in Hosted Zone {HOSTED_ZONE_NAME} is not a dangling IP."
)