
Secure Your AWS Application Load Balancer: Dropping Invalid HTTP Headers
HTTP headers are vital for web communication, carrying critical information like cookies and authorization tokens. But what happens when these headers aren't properly formatted? Malicious actors can exploit header vulnerabilities to compromise your application. This article shows you how to mitigate this risk by enabling the "drop invalid headers" feature in AWS Application Load Balancers (ALB).
The Overlooked Security Setting: drop_invalid_header_fields
When configuring an AWS ALB, you likely focus on health checks, SSL certificates, and security groups. The drop_invalid_header_fields
setting is often overlooked. Aqua Sec identifies the absence of this setting as a high-risk vulnerability (AVD-AWS-0052).
Enabling this feature is straightforward and offers a significant security boost. However, remember that your application might rely on certain non-standard headers. Ensure compatibility before enabling this feature, as improper implementation might affect functionalities tied to unique non-standard headers.
Here's how to enable "drop invalid headers" in Terraform:
resource "aws_alb" "ALB" {
name = "my-load-balancer"
security_groups = [data.aws_security_group.alb_sg.id]
drop_invalid_header_fields = true
# ... other configurations
}
Testing the ALB: A Practical Example
Let's create a basic ALB that returns a “Fixed response!” message. We'll use Terraform and OpenTofu for deployment. This setup will allow us to experiment with valid and invalid headers.
We will need:
- A VPC with public subnets, Internet Gateway, and route tables. The
aws-ia/vpc
module simplifies VPC creation. - A security group permitting connections to the ALB.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-2"
}
module "vpc" {
source = "aws-ia/vpc/aws"
version = ">= 4.2.0"
name = "alb-example"
cidr_block = var.vpc_cidr
az_count = 2
subnets = {
public = {
netmask = 24
}
}
}
resource "aws_security_group" "alb_security_group" {
name = "alb-security-group"
vpc_id = module.vpc.vpc_attributes.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_alb" "sample" {
name = "sample-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_security_group.id]
subnets = values(module.vpc.public_subnet_attributes_by_az)[*].id
}
resource "aws_alb_listener" "listener" {
load_balancer_arn = aws_alb.sample.arn
port = "80"
protocol = "HTTP"
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/html"
message_body = "<html><body><h3>Fixed response!</h3></body></html>"
status_code = "200"
}
}
}
output "alb_dns_name" {
value = aws_alb.sample.dns_name
}
Once deployed, use curl
to verify the ALB's functionality:
Testing Invalid Headers: Before and After
Now, let's send requests with both valid and invalid headers. Initially, we'll leave the drop_invalid_header_fields
setting disabled to observe the ALB's behavior:
Without the setting enabled, the ALB accepts all headers, including the invalid one.
Next, we'll enable the drop_invalid_header_fields
setting and repeat the test:
resource "aws_alb" "sample" {
name = "sample-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_security_group.id]
subnets = values(module.vpc.public_subnet_attributes_by_az)[*].id
drop_invalid_header_fields = true
}
Even with verbose curl
, you won't see an error. The invalid header is silently dropped. To verify header dropping, we can turn to access logs.
Diving into Access Logs
Enable ALB access logs to gain insights into header handling. You'll need an S3 bucket for log storage and a policy granting the ALB write access:
locals {
elb_account_id = "652711504416" # ELB account of eu-west-2
}
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket" "logs_bucket" {
bucket = "logs-bucket-5535325"
}
# This policy should restrict the path to which ELB can write because if we just
# put '*' in Resource, it will allow anyone to use our bucket for their ALB
# logs.
resource "aws_s3_bucket_policy" "log_delivery_policy" {
bucket = aws_s3_bucket.logs_bucket.bucket
policy = <<-EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::${local.elb_account_id}:root",
"Service": "logdelivery.elasticloadbalancing.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::${aws_s3_bucket.logs_bucket.bucket}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"
}
]
}
EOF
}
resource "aws_alb" "sample" {
name = "sample-alb"
# ... other configurations
access_logs {
bucket = aws_s3_bucket.logs_bucket.bucket
enabled = true
}
}
Analyze the logs by downloading them from S3 and unzipping them.
While the logs don't explicitly identify dropped headers, you can infer their presence by comparing the received_bytes
field for requests with and without invalid headers. Larger values indicate additional headers.
The drop_invalid_header_fields
setting focuses on protecting backend services, not the ALB itself. The primary goal of this feature is to prevent malicious headers from reaching your applications.
Verifying Header Transmission with Lambda
To confirm how headers are handled, create an AWS Lambda function that echoes the received headers:
Deploy the Lambda:
resource "aws_iam_role" "lambda_role" {
name = "lambda_alb_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic_execution" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
data "archive_file" "lambda_zip" {
type = "zip"
source_content = file("${path.module}/lambda.py")
source_content_filename = "lambda.py"
output_path = "${path.module}/lambda.zip"
}
resource "aws_lambda_function" "headers_lambda" {
filename = data.archive_file.lambda_zip.output_path
function_name = "display-request-headers"
role = aws_iam_role.lambda_role.arn
handler = "lambda.lambda_handler"
runtime = "python3.12"
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
timeout = 15
memory_size = 128
}
# Permission for ALB to invoke Lambda
resource "aws_lambda_permission" "alb_invoke" {
statement_id = "AllowALBInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.headers_lambda.function_name
principal = "elasticloadbalancing.amazonaws.com"
}
# Target group with Lambda
resource "aws_lb_target_group" "lambda_tg" {
name = "lambda-headers-tg"
target_type = "lambda"
}
resource "aws_lb_target_group_attachment" "lambda_tg_attachment" {
target_group_arn = aws_lb_target_group.lambda_tg.arn
target_id = aws_lambda_function.headers_lambda.arn
depends_on = [aws_lambda_permission.alb_invoke]
}
Disable the drop_invalid_header_fields
setting temporarily and configure the ALB listener to forward requests to the Lambda function:
resource "aws_alb" "sample" {
name = "sample-alb"
drop_invalid_header_fields = false
#... Other configurations
}
resource "aws_alb_listener" "listener" {
load_balancer_arn = aws_alb.sample.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.lambda_tg.arn
}
}
Test with curl
:
Observe how the Lambda function receives and prints all headers, including invalid ones. Re-enable the drop_invalid_header_fields
setting to see the difference.
Final Thoughts: Protect Your Application
By enabling the drop_invalid_header_fields
setting in your AWS Application Load Balancer, you add a crucial layer of defense against potential exploits. While it won't protect the ALB itself, it ensures that your backend applications receive only valid, well-formatted headers, minimizing the risk of header-based attacks. Regularly review your ALB configuration and access logs to maintain a robust security posture. Remember to analyze your application's header requirements, as improper implementation might impair important functionalities.