From 6f172a5c19886ee43e9ba1b5c19d7ce11ff6aaa1 Mon Sep 17 00:00:00 2001 From: potato-20 <164017049+potato-20@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:56:07 +0530 Subject: [PATCH] feat(elbv2): add elbv2_alb_drop_invalid_header_fields_enabled check (FSBP ELB.4) (#11471) Co-authored-by: Hugo P.Brito --- prowler/CHANGELOG.md | 1 + ...ndational_security_best_practices_aws.json | 4 +- .../__init__.py | 0 ...nvalid_header_fields_enabled.metadata.json | 40 +++ ..._alb_drop_invalid_header_fields_enabled.py | 27 ++ ...drop_invalid_header_fields_enabled_test.py | 254 ++++++++++++++++++ 6 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/__init__.py create mode 100644 prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.metadata.json create mode 100644 prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.py create mode 100644 tests/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 5d180cb4e3..6922846b0d 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `sagemaker_models_monitor_enabled` check for AWS provider, verifying that each SageMaker monitoring schedule is in the `Scheduled` state so data and model drift is actively detected [(#11278)](https://github.com/prowler-cloud/prowler/pull/11278) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) universal compliance framework with AWS provider coverage across the five DORA pillars [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131) +- `elbv2_alb_drop_invalid_header_fields_enabled` check for AWS provider, verifying Application Load Balancers have `routing.http.drop_invalid_header_fields.enabled` set to `true` to mitigate HTTP desync attacks (AWS FSBP ELB.4) [(#11471)](https://github.com/prowler-cloud/prowler/pull/11471) --- diff --git a/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json b/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json index a64a421c8a..cea7ad1655 100644 --- a/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json +++ b/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json @@ -1863,7 +1863,9 @@ "Id": "ELB.4", "Name": "Application load balancers should be configured to drop HTTP headers", "Description": "This control evaluates AWS Application Load Balancers (ALB) to ensure they are configured to drop invalid HTTP headers. The control fails if the value of routing.http.drop_invalid_header_fields.enabled is set to false. By default, ALBs are not configured to drop invalid HTTP header values. Removing these header values prevents HTTP desync attacks.", - "Checks": [], + "Checks": [ + "elbv2_alb_drop_invalid_header_fields_enabled" + ], "Attributes": [ { "ItemId": "ELB.4", diff --git a/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/__init__.py b/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.metadata.json new file mode 100644 index 0000000000..0293501578 --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.metadata.json @@ -0,0 +1,40 @@ +{ + "Provider": "aws", + "CheckID": "elbv2_alb_drop_invalid_header_fields_enabled", + "CheckTitle": "Application Load Balancer should be configured to drop invalid HTTP header fields", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability", + "Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices", + "TTPs/Initial Access", + "Effects/Data Exposure" + ], + "ServiceName": "elbv2", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsElbv2LoadBalancer", + "ResourceGroup": "network", + "Description": "Ensure that Application Load Balancers (ALB) are configured to drop invalid HTTP header fields. The check fails when `routing.http.drop_invalid_header_fields.enabled` is not set to `true`. By default, ALBs do not remove HTTP headers that do not conform to RFC 7230.", + "Risk": "Forwarding non-RFC-compliant HTTP headers to backend targets enables HTTP desync (request smuggling):\n- **Confidentiality**: session/token theft, data exfiltration\n- **Integrity**: cache poisoning, request routing bypass, unauthorized actions\n- **Availability**: backend exhaustion.\nDropping invalid header fields removes a primary smuggling vector.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#drop-invalid-header-fields", + "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-4" + ], + "Remediation": { + "Code": { + "CLI": "aws elbv2 modify-load-balancer-attributes --load-balancer-arn --attributes Key=routing.http.drop_invalid_header_fields.enabled,Value=true", + "NativeIaC": "```yaml\n# CloudFormation: enable drop invalid header fields on an ALB\nResources:\n :\n Type: AWS::ElasticLoadBalancingV2::LoadBalancer\n Properties:\n Type: application\n Subnets:\n - \n - \n LoadBalancerAttributes:\n - Key: routing.http.drop_invalid_header_fields.enabled # Critical: drop non-RFC-compliant headers\n Value: true\n```", + "Other": "1. Open the Amazon EC2 console and choose Load Balancers.\n2. Select the Application Load Balancer.\n3. On the Attributes tab, choose Edit.\n4. Set 'Drop invalid header fields' to Enabled.\n5. Save changes.", + "Terraform": "```hcl\n# Terraform: enable drop invalid header fields on an ALB\nresource \"aws_lb\" \"\" {\n name = \"\"\n load_balancer_type = \"application\"\n subnets = [\"\", \"\"]\n drop_invalid_header_fields = true # Critical: drop non-RFC-compliant headers\n}\n```" + }, + "Recommendation": { + "Text": "Enable 'drop invalid header fields' on Application Load Balancers so non-RFC-compliant HTTP headers are removed before requests reach backend targets, reducing exposure to HTTP desync and request smuggling. Apply defense in depth and validate requests at the application layer as well.", + "Url": "https://hub.prowler.com/check/elbv2_alb_drop_invalid_header_fields_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.py b/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.py new file mode 100644 index 0000000000..86740a253a --- /dev/null +++ b/prowler/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elbv2.elbv2_client import elbv2_client + + +class elbv2_alb_drop_invalid_header_fields_enabled(Check): + def execute(self): + findings = [] + for lb in elbv2_client.loadbalancersv2.values(): + if lb.type == "application": + report = Check_Report_AWS( + metadata=self.metadata(), + resource=lb, + ) + report.status = "PASS" + report.status_extended = ( + f"ELBv2 ALB {lb.name} is configured to drop invalid " + "header fields." + ) + if lb.drop_invalid_header_fields != "true": + report.status = "FAIL" + report.status_extended = ( + f"ELBv2 ALB {lb.name} is not configured to drop " + "invalid header fields." + ) + findings.append(report) + + return findings diff --git a/tests/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled_test.py b/tests/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled_test.py new file mode 100644 index 0000000000..40d8d91f0d --- /dev/null +++ b/tests/providers/aws/services/elbv2/elbv2_alb_drop_invalid_header_fields_enabled/elbv2_alb_drop_invalid_header_fields_enabled_test.py @@ -0,0 +1,254 @@ +from importlib import import_module +from unittest import mock + +from boto3 import client, resource +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_WEST_1_AZA, + AWS_REGION_EU_WEST_1_AZB, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +CHECK_MODULE = ( + "prowler.providers.aws.services.elbv2." + "elbv2_alb_drop_invalid_header_fields_enabled." + "elbv2_alb_drop_invalid_header_fields_enabled" +) +ELBV2_CLIENT_PATCH = f"{CHECK_MODULE}.elbv2_client" +GLOBAL_PROVIDER_PATCH = ".".join( + [ + "prowler.providers.common.provider.Provider", + "get_global_provider", + ] +) +PASS_STATUS_EXTENDED = " ".join( + [ + "ELBv2 ALB my-lb is configured to drop invalid", + "header fields.", + ] +) +FAIL_STATUS_EXTENDED = ( + "ELBv2 ALB my-lb is not configured to drop invalid header fields." +) + + +def get_check_class(): + return getattr( + import_module(CHECK_MODULE), + "elbv2_alb_drop_invalid_header_fields_enabled", + ) + + +class Test_elbv2_alb_drop_invalid_header_fields_enabled: + @mock_aws + def test_elb_no_balancers(self): + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + with ( + mock.patch( + GLOBAL_PROVIDER_PATCH, + return_value=set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ), + ), + mock.patch( + ELBV2_CLIENT_PATCH, + new=ELBv2( + set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + ), + ), + ): + check = get_check_class()() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_elbv2_dropping_invalid_header_fields(self): + conn = client("elbv2", region_name=AWS_REGION_EU_WEST_1) + ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + vpc = ec2.create_vpc( + CidrBlock="172.28.7.0/24", + InstanceTenancy="default", + ) + subnet1 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.192/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + ) + subnet2 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.0/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + ) + + lb = conn.create_load_balancer( + Name="my-lb", + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme="internal", + Type="application", + )["LoadBalancers"][0] + + conn.modify_load_balancer_attributes( + LoadBalancerArn=lb["LoadBalancerArn"], + Attributes=[ + { + "Key": "routing.http.drop_invalid_header_fields.enabled", + "Value": "true", + }, + ], + ) + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + with ( + mock.patch( + GLOBAL_PROVIDER_PATCH, + return_value=set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ), + ), + mock.patch( + ELBV2_CLIENT_PATCH, + new=ELBv2( + set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + ), + ), + ): + check = get_check_class()() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == PASS_STATUS_EXTENDED + assert result[0].resource_id == "my-lb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_elbv2_not_dropping_invalid_header_fields(self): + conn = client("elbv2", region_name=AWS_REGION_EU_WEST_1) + ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + vpc = ec2.create_vpc( + CidrBlock="172.28.7.0/24", + InstanceTenancy="default", + ) + subnet1 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.192/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + ) + subnet2 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.0/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + ) + + lb = conn.create_load_balancer( + Name="my-lb", + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme="internal", + Type="application", + )["LoadBalancers"][0] + + conn.modify_load_balancer_attributes( + LoadBalancerArn=lb["LoadBalancerArn"], + Attributes=[ + { + "Key": "routing.http.drop_invalid_header_fields.enabled", + "Value": "false", + }, + ], + ) + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + with ( + mock.patch( + GLOBAL_PROVIDER_PATCH, + return_value=set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ), + ), + mock.patch( + ELBV2_CLIENT_PATCH, + new=ELBv2( + set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + ), + ), + ): + check = get_check_class()() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == FAIL_STATUS_EXTENDED + assert result[0].resource_id == "my-lb" + assert result[0].resource_arn == lb["LoadBalancerArn"] + + @mock_aws + def test_elbv2_network_load_balancer_ignored(self): + conn = client("elbv2", region_name=AWS_REGION_EU_WEST_1) + ec2 = resource("ec2", region_name=AWS_REGION_EU_WEST_1) + + vpc = ec2.create_vpc( + CidrBlock="172.28.7.0/24", + InstanceTenancy="default", + ) + subnet1 = ec2.create_subnet( + VpcId=vpc.id, + CidrBlock="172.28.7.192/26", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + ) + + conn.create_load_balancer( + Name="my-nlb", + Subnets=[subnet1.id], + Scheme="internal", + Type="network", + ) + + from prowler.providers.aws.services.elbv2.elbv2_service import ELBv2 + + with ( + mock.patch( + GLOBAL_PROVIDER_PATCH, + return_value=set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ), + ), + mock.patch( + ELBV2_CLIENT_PATCH, + new=ELBv2( + set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1], + create_default_organization=False, + ) + ), + ), + ): + check = get_check_class()() + result = check.execute() + + assert len(result) == 0