mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
fix(route53): resolve false positive in dangling IP check (#9952)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user