# Exploiting Improper Validation of Amazon Simple Notification Service SigningCertUrl

AWS SNS SDK validators trusted SigningCertURL host regex that matched the attacker-writable public S3 bucket 'sns', letting attackers forge signed SNS webhooks to any subscriber.

- Payout: Research
- Severity: high
- Program: Amazon
- Write-up by: Eugene Lim
- Disclosed: 2022-08-29
- Write-up quality: 8/10
- Original: https://spaceraccoon.dev/exploiting-improper-validation-amazon-simple-notification-service/
- Library page: https://bugbountylibrary.com/r/blog%3Aspaceraccoon-dev-exploiting-improper-validation-amazon-simple-notification-service
- Tags: area:cloud domain:cloud impact:integrity kind:research platform:research sev:high tech:aws tech:s3 technique:recon technique:source-review vuln:auth-bypass vuln:signature-replay

**Target:** AWS SNS HTTP/S webhook signature validation (sns-validator NPM and custom validators)
**Root cause:** Signature verification fetches the public key from the attacker-supplied SigningCertURL; the SDK's host regex ^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$ also matches the S3 virtual-host form sns.s3-us-west-2.amazonaws.com, and the 'sns' S3 bucket was unreserved and publicly writable.
**Steps:**
1. Observe SNS POST body contains SigningCertURL used to fetch the signing cert with no authenticity check.
2. Note SDK regex requires host starting with `sns.` and ending `.amazonaws.com`; S3 virtual-host `sns.s3-us-west-2.amazonaws.com` satisfies it (second slug >=3 chars).
3. Confirm bucket `sns` is unreserved, publicly readable and writable.
4. Upload own keypair's public cert as `https://sns.s3-us-west-2.amazonaws.com/evil.pem`.
5. Craft SNS JSON body with chosen Message/MessageId/Subject/Timestamp/TopicArn/Type, set SigningCertURL to the evil.pem URL, sign canonical string with own private key, base64 into Signature.
6. POST forged body to victim webhook endpoint (e.g. `https://monitor.firefox.com/ses/notification`); validator fetches attacker cert and accepts signature.
**How found:** Reading the AWS signature-verification docs and sample Python script revealed SigningCertURL is fetched without validation; reviewing StackOverflow-style regexes and the sns-validator NPM regex showed the S3 virtual-host form passes the host check, prompting a check of whether the 'sns' bucket was reserved/writable.
**Exploitation:** Attacker generates a keypair, uploads the public cert to the public-writable 'sns' S3 bucket, signs an arbitrary SNS message payload with the matching private key, and POSTs it to a victim's SNS webhook endpoint. The SDK validator downloads the attacker cert from SigningCertURL, verifies the signature successfully, and the application processes the forged message as authentic (e.g. Firefox Monitor deletes arbitrary subscribers via forged SES bounce/complaint events).
**Escalation:** Chaining forged SNS messages to application-specific handlers turns signature bypass into arbitrary data manipulation, e.g. deleting arbitrary users from Firefox Monitor's database via forged bounce/complaint events.
**Insight:** When a signature scheme fetches its trust anchor from an attacker-influenced URL, enumerate every hostname form that passes the validator's regex — S3 virtual-host subdomains (bucket.s3-region.amazonaws.com) often satisfy 'ends with amazonaws.com' checks, so test whether the required bucket name is unclaimed and writable.

## Proof of concept (copied exactly from the original report)

**Forged SNS message with attacker-controlled SigningCertURL**

```
POST / HTTP/1.1
x-amz-sns-message-type: Notification
x-amz-sns-message-id: 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324
x-amz-sns-topic-arn: arn:aws:sns:us-west-2:123456789012:MyTopic
x-amz-sns-subscription-arn: arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96
Content-Length: 773
Content-Type: text/plain; charset=UTF-8
Host: example.com
Connection: Keep-Alive
User-Agent: Amazon Simple Notification Service Agent
 
{
 "Type" : "Notification",
 "MessageId" : "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
 "TopicArn" : "arn:aws:sns:us-west-2:123456789012:MyTopic",
 "Subject" : "My First Message",
 "Message" : "Hello world!",
 "Timestamp" : "2012-05-02T00:54:06.655Z",
 "SignatureVersion" : "1",
 "Signature" : "EXAMPLEw6JRN...",
 "SigningCertURL" : "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
```

**sns-validator regex allowing S3 subdomain bypass**

```
defaultHostPattern = /^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/,

// hostPattern defaults to defaultHostPattern
var validateUrl = function (urlToValidate, hostPattern) {
 var parsed = url.parse(urlToValidate);

 return parsed.protocol === 'https:'
 && parsed.path.substr(-4) === '.pem'
 && hostPattern.test(parsed.host);
};
```

**Firefox Monitor vulnerable SNS webhook handler**

```
'use strict'

const MessageValidator = require('sns-validator')

const DB = require('../db/DB')

const mozlog = require('../log')

const validator = new MessageValidator()
const log = mozlog('controllers.ses')

async function notification (req, res) {
 const message = JSON.parse(req.body)
 return new Promise((resolve, reject) => {
 validator.validate(message, async (err, message) => {
 if (err) {
 log.error('notification', { err })
 const body = 'Access denied. ' + err.message
 res.status(401).send(body)
 return reject(body)
 }

 await handleNotification(message)

 res.status(200).json(
 { status: 'OK' }
 )
 return resolve('OK')
 })
 })
}
```

