Application Load Balancer - Drop Invalid Headers
We use HTTP headers all the time. In requests we do, we often send cookies, authorization headers with bearer tokens, content length, we get responses with new cookies, expiration dates for caching. We don't think too much about this, even when building things like a REST application. However, it is a nice point for malicious actors to try exploiting our vulnerabilities. That's why today I want to demonstrate you how "drop invalid headers" feature was implemented in AWS Elastic Load Balancer. Code for this post available on GitHub The mysterious setting When creating an ALB in AWS, not many of us think about this setting. We mostly focus on health checks, SSL certificates, security groups, draining timeout. And this setting just sits there untouched. According to Aqua Sec, lack of this setting is makred as high risk AVD-AWS-0052. However, it is very easy to enable. What we need to keep in mind is that our application might not like it if we didn't conform to standards. To enable this feature in Terraform we need to just add this third line: resource "aws_alb" "ALB" { name = "my-load-balancer" security_groups = [data.aws_security_group.alb_sg.id] drop_invalid_header_fields = true ... } Let's build a simple ALB that will just return a fixed response. In order to do this I prepare a Terraform that I will deploy with OpenTofu. As this is just a test, there's no need for SSL certificates or HTTPS listener - the secure one will work the same way as plaintext listener. In order to create an Elastic Load Balancer, we need a VPC in our AWS environment (including public subnets, Internet Gateway, route tables, etc.) and a security group that will allow us to connect to the ELB. For simplicity, I will use here aws-ia/vpc module that will allow us to construct almost all the components with minimal work required (and make it cleaner than any LLM output). 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 } } } Now in a new file we can define our load balancer with a security group, listener and a fixed response. Useful for us will also be the output with the URL we can visit to see the response. 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 = "Fixed response!" status_code = "200" } } } output "alb_dns_name" { value = aws_alb.sample.dns_name } For now the load balancer should just reply to us with some HTML. We can test it in the browser as well as using curl in our Terminal. The latter will be later useful to provide some invalid headers. Let's see if the ALB is at least functional. # Or simply copy and paste the URL after http:// $ curl http://$(tofu output -raw alb_dns_name) Fixed response! Good to see that we have a response. We can turn on verbose mode to see everything that is happening under the hood, all the headers going around and HTTP status code. $ curl -v http://$(tofu output -raw alb_dns_name) * Host sample-alb-1234567.eu-west-2.elb.amazonaws.com:80 was resolved. * IPv6: (none) * IPv4: 3.11.255.255 * Trying 3.11.255.255:80... * Connected to sample-alb-1234567.eu-west-2.elb.amazonaws.com (3.11.255.255) port 80 > GET / HTTP/1.1 > Host: sample-alb-1234567.eu-west-2.elb.amazonaws.com > User-Agent: curl/8.7.1 > Accept: */* > * Request completely sent off < HTTP/1.1 200 OK < Date: Sat, 19 Apr 2025 07:13:47 GMT < Content-Type: text/html; charset=utf-8 < Content-Length: 50 < Connection: keep-alive < * Connection #0 to host sample-alb-1234567.eu-west-2.elb.amazonaws.com left intact Fixed response! Trying invalid headers I will try now sending some headers. One of them should be invalid. I will not turn on the setting so that all the headers should be accepted. $ curl -v \ -H 'X-Custom-Header: valid-header' \ -H 'X-!NotValid@Header

We use HTTP headers all the time. In requests we do, we often send cookies, authorization headers with bearer tokens, content length, we get responses with new cookies, expiration dates for caching. We don't think too much about this, even when building things like a REST application. However, it is a nice point for malicious actors to try exploiting our vulnerabilities. That's why today I want to demonstrate you how "drop invalid headers" feature was implemented in AWS Elastic Load Balancer.
Code for this post available on GitHub
The mysterious setting
When creating an ALB in AWS, not many of us think about this setting. We mostly focus on health checks, SSL certificates, security groups, draining timeout. And this setting just sits there untouched. According to Aqua Sec, lack of this setting is makred as high risk AVD-AWS-0052.
However, it is very easy to enable. What we need to keep in mind is that our application might not like it if we didn't conform to standards. To enable this feature in Terraform we need to just add this third line:
resource "aws_alb" "ALB" {
name = "my-load-balancer"
security_groups = [data.aws_security_group.alb_sg.id]
drop_invalid_header_fields = true
...
}
Let's build a simple ALB that will just return a fixed response. In order to do this I prepare a Terraform that I will deploy with OpenTofu. As this is just a test, there's no need for SSL certificates or HTTPS listener - the secure one will work the same way as plaintext listener.
In order to create an Elastic Load Balancer, we need a VPC in our AWS environment (including public subnets, Internet Gateway, route tables, etc.) and a security group that will allow us to connect to the ELB. For simplicity, I will use here aws-ia/vpc
module that will allow us to construct almost all the components with minimal work required (and make it cleaner than any LLM output).
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 }
}
}
Now in a new file we can define our load balancer with a security group, listener and a fixed response. Useful for us will also be the output with the URL we can visit to see the response.
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 = "Fixed response!
"
status_code = "200"
}
}
}
output "alb_dns_name" {
value = aws_alb.sample.dns_name
}
For now the load balancer should just reply to us with some HTML. We can test it in the browser as well as using curl
in our Terminal. The latter will be later useful to provide some invalid headers. Let's see if the ALB is at least functional.
# Or simply copy and paste the URL after http://
$ curl http://$(tofu output -raw alb_dns_name)
Fixed response!
Good to see that we have a response. We can turn on verbose mode to see everything that is happening under the hood, all the headers going around and HTTP status code.
$ curl -v http://$(tofu output -raw alb_dns_name)
* Host sample-alb-1234567.eu-west-2.elb.amazonaws.com:80 was resolved.
* IPv6: (none)
* IPv4: 3.11.255.255
* Trying 3.11.255.255:80...
* Connected to sample-alb-1234567.eu-west-2.elb.amazonaws.com (3.11.255.255) port 80
> GET / HTTP/1.1
> Host: sample-alb-1234567.eu-west-2.elb.amazonaws.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Sat, 19 Apr 2025 07:13:47 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 50
< Connection: keep-alive
<
* Connection #0 to host sample-alb-1234567.eu-west-2.elb.amazonaws.com left intact
Fixed response!
Trying invalid headers
I will try now sending some headers. One of them should be invalid. I will not turn on the setting so that all the headers should be accepted.
$ curl -v \
-H 'X-Custom-Header: valid-header' \
-H 'X-!NotValid@Header