Compare commits

...
Author SHA1 Message Date
Andrey Antukh 64fe8e57e3 🐛 Fix SNS signature verification field sets per AWS spec
V1 and V2 use the same field sets (only hash algorithm differs).
The AWS documentation explicitly shows that SigningCertURL and
SignatureVersion are metadata fields, not part of the signed content.

Changes:
- Remove V2-specific field lists (SigningCertURL, SignatureVersion)
- Simplify build-string-to-sign to use unified field sets
- Fix resource leak in fetch-certificate (close stream on non-200)
- Fix missing Message field returning 200 instead of 400
- Stub fetch-certificate in network-dependent test
- Update V2 test to reflect correct field sets

Reference: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message-verify-message-signature.html

AI-assisted-by: qwen3.7-plus
2026-08-05 18:22:19 +00:00
Andrey Antukh a4b9ccee8b 🐛 Fix SNS signature verification field sets per AWS spec
Address critical code review findings:

- Remove 'Signature' field from string-to-sign (it's the output, not input)
- Differentiate V1 vs V2 field sets per AWS SNS documentation:
  - V1 Notification: Message, MessageId, Subject, Timestamp, TopicArn, Type
  - V1 SubscriptionConfirmation: adds Token, SubscribeURL (excludes SigningCertURL, SignatureVersion)
  - V2 Notification/Subscription: all fields except Signature
- Add 'Token' field to SubscriptionConfirmation (required by AWS spec)
- Add AWS documentation URL comments for future reference
- Improve error handling in fetch-certificate and verify-signature
- Add end-to-end signature verification tests with real key pairs
- Add test resources (certificate and private key) for signature tests

See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html

Closes #11092

AI-assisted-by: qwen3.7-plus
2026-08-05 16:00:09 +00:00
Andrey Antukh 2101d657e5 🐛 Fix SNS signature verification issues
Address code review findings:

- Restrict URL validation to sns.<region>.amazonaws.com only
  (prevents attacker-controlled S3 buckets from being accepted)
- Add support for SignatureVersion 2 (SHA256withRSA)
- Fix resource leak by wrapping certificate stream in with-open
- Return proper HTTP status codes (4xx for invalid messages,
  5xx for transient failures)
- Add comprehensive tests for URL validation, signature versions,
  and HTTP response codes

Closes #11092

AI-assisted-by: qwen3.7-plus
2026-08-05 14:57:26 +00:00
Andrey Antukh a8a441b7ad 🐛 Verify AWS SNS signature on webhook notifications
The /webhooks/sns endpoint was accepting bounce and complaint
notifications without verifying the AWS SNS cryptographic signature.
This allowed any authenticated user to forge reports for arbitrary
email addresses using their own valid :profile-identity token.

The fix adds:
- AWS SNS signature verification using RSA-SHA1
- URL validation for SigningCertURL and SubscribeURL (must be from
  amazonaws.com domain)
- Proper logging of all verification failures with context fields
- Rejection of unverified messages before any processing

Closes #11092

AI-assisted-by: qwen3.7-plus
2026-08-05 12:38:46 +00:00
5 changed files with 518 additions and 12 deletions

No files matched your search

+1 -1
View File
@@ -77,7 +77,7 @@
org.clojure/data.csv {:mvn/version "1.1.1"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
mockery/mockery {:mvn/version "0.1.4"}}
:extra-paths ["test" "dev"]}
:extra-paths ["test" "test/resources" "dev"]}
:build
{:extra-deps
+183 -11
View File
@@ -21,13 +21,158 @@
[cuerdas.core :as str]
[integrant.core :as ig]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
[yetti.response :as-alias yres])
(:import
java.net.URI
java.security.cert.CertificateFactory
java.security.Signature
java.util.Base64))
(declare parse-json)
(declare handle-request)
(declare parse-notification)
(declare process-report)
(defn- valid-sns-url?
"Validates that a URL originates from an SNS endpoint.
Only accepts sns.<region>.amazonaws.com hosts.
See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html"
[url]
(when (string? url)
(try
(let [uri (URI. url)
host (.getHost uri)]
(and (= "https" (.getScheme uri))
(boolean
(re-matches
#"(?i)sns\.[a-z0-9-]+\.amazonaws\.com"
host))))
(catch Exception _
false))))
;; AWS SNS Signature Verification Field Sets
;; See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message-verify-message-signature.html
;;
;; V1 and V2 use the SAME field sets (only the hash algorithm differs: SHA1 vs SHA256)
;; The "Signature" field is NEVER part of the string-to-sign (it's the output, not input)
;; "SigningCertURL" and "SignatureVersion" are metadata, not signed
;; Notification: Message, MessageId, Subject (if present), Timestamp, TopicArn, Type
(def ^:private notification-fields
["Message" "MessageId" "Subject" "Timestamp" "TopicArn" "Type"])
;; SubscriptionConfirmation: Message, MessageId, SubscribeURL, Timestamp, Token, TopicArn, Type
(def ^:private subscription-fields
["Message" "MessageId" "SubscribeURL" "Timestamp" "Token" "TopicArn" "Type"])
(defn- build-string-to-sign
"Builds the string-to-sign for AWS SNS signature verification.
V1 and V2 use the same field sets (only hash algorithm differs: SHA1 vs SHA256).
See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message-verify-message-signature.html"
[body]
(let [msg-type (get body "Type")
fields (if (= "SubscriptionConfirmation" msg-type)
subscription-fields
notification-fields)]
(->> fields
(filter #(contains? body %))
(map #(str % "\n" (get body %) "\n"))
(apply str))))
(defn- fetch-certificate
"Fetches the X.509 certificate from the given URL.
Returns an InputStream that must be closed by the caller.
See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html"
[cfg cert-url]
(let [response (http/req cfg {:uri cert-url :method :get :timeout 10000}
{:sync? true :response-type :input-stream})]
(when-not (= 200 (:status response))
(when-let [body (:body response)]
(.close ^java.io.Closeable body))
(l/wrn :hint "failed to fetch SNS signing certificate"
:action "sns-cert-fetch-failed"
:status (:status response)
:cert-url cert-url)
(ex/raise :type :internal :code :cert-fetch-failed))
(:body response)))
(defn- verify-signature
"Verifies the RSA signature of the message.
See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html"
[cfg body]
(let [cert-url (get body "SigningCertURL")
signature (get body "Signature")
sig-version (get body "SignatureVersion")
algorithm (case sig-version
"1" "SHA1withRSA"
"2" "SHA256withRSA"
nil)]
(when-not algorithm
(throw (ex-info "Unsupported SNS signature version"
{:type :validation :version sig-version})))
(when (and cert-url signature)
(try
(let [string-sign (build-string-to-sign body)]
(with-open [cert-stream (fetch-certificate cfg cert-url)]
(let [cf (CertificateFactory/getInstance "X.509")
cert (.generateCertificate cf cert-stream)
sig (Signature/getInstance algorithm)]
(.initVerify sig (.getPublicKey cert))
(.update sig (.getBytes string-sign java.nio.charset.StandardCharsets/UTF_8))
(.verify sig (.decode (Base64/getDecoder) signature)))))
(catch clojure.lang.ExceptionInfo e
(let [data (ex-data e)]
(if (= :validation (:type data))
(throw e)
(do
(l/wrn :hint "SNS signature verification exception"
:action "sns-signature-verification-exception"
:cause e)
false))))
(catch Exception e
(l/wrn :hint "SNS signature verification exception"
:action "sns-signature-verification-exception"
:cause e)
false)))))
(defn- verify-sns-message!
"Verifies the AWS SNS message signature and URL validity.
Throws if verification fails.
See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html"
[cfg body]
(let [cert-url (get body "SigningCertURL")
subscribe-url (get body "SubscribeURL")
mtype (get body "Type")]
(when-not (valid-sns-url? cert-url)
(l/wrn :hint "SNS certificate URL not from amazonaws.com"
:action "sns-invalid-cert-url"
:message-type mtype
:signing-cert-url cert-url)
(ex/raise :type :validation
:code :invalid-signing-cert-url
:hint "SigningCertURL must be from amazonaws.com"))
(when (and (= mtype "SubscriptionConfirmation")
(not (valid-sns-url? subscribe-url)))
(l/wrn :hint "SNS subscribe URL not from amazonaws.com"
:action "sns-invalid-subscribe-url"
:message-type mtype
:subscribe-url subscribe-url)
(ex/raise :type :validation
:code :invalid-subscribe-url
:hint "SubscribeURL must be from amazonaws.com"))
(when-not (verify-signature cfg body)
(l/wrn :hint "SNS signature verification failed"
:action "sns-signature-verification-failed"
:message-type mtype
:topic-arn (get body "TopicArn")
:signing-cert-url cert-url)
(ex/raise :type :authentication
:code :invalid-signature
:hint "SNS signature verification failed"))))
(defmethod ig/assert-key ::routes
[_ params]
(assert (http/client? (::http/client params)) "expect a valid http client")
@@ -37,9 +182,9 @@
(defmethod ig/init-key ::routes
[_ cfg]
(letfn [(handler [request]
(let [data (-> request yreq/body slurp)]
(handle-request cfg data)
{::yres/status 200}))]
(let [data (-> request yreq/body slurp)
result (handle-request cfg data)]
{::yres/status (or (:status result) 200)}))]
["/sns" {:handler handler
:allowed-methods #{:post}}]))
@@ -48,25 +193,52 @@
(try
(let [body (parse-json data)
mtype (get body "Type")]
(when body
(verify-sns-message! cfg body))
(cond
(= mtype "SubscriptionConfirmation")
(let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")]
(l/info :action "subscription received" :topic stopic :url surl)
(http/req cfg {:uri surl :method :post :timeout 10000} {:sync? true}))
(http/req cfg {:uri surl :method :post :timeout 10000} {:sync? true})
{:status 200})
(= mtype "Notification")
(when-let [message (parse-json (get body "Message"))]
(let [notification (parse-notification cfg message)]
(process-report cfg notification)))
(if-let [message (parse-json (get body "Message"))]
(do
(let [notification (parse-notification cfg message)]
(process-report cfg notification))
{:status 200})
(do
(l/wrn :hint "notification with missing or unparseable Message field"
:action "sns-missing-message")
{:status 400}))
:else
(l/warn :hint "unexpected data received"
:report (pr-str body))))
(do
(l/warn :hint "unexpected data received"
:report (pr-str body))
{:status 400})))
(catch clojure.lang.ExceptionInfo e
(let [data (ex-data e)]
(if (#{:validation :authentication} (:type data))
(do
(l/wrn :hint "SNS message validation failed"
:action "sns-validation-failed"
:code (:code data))
{:status 400})
(do
(l/error :hint "unexpected exception on awsns"
:cause e)
{:status 500}))))
(catch Throwable cause
(l/error :hint "unexpected exception on awsns"
:cause cause))))
:cause cause)
{:status 500})))
(defn- parse-bounce
[data]
@@ -12,6 +12,7 @@
[app.http.awsns :as awsns]
[app.tokens :as tokens]
[backend-tests.helpers :as th]
[clojure.data.json :as j]
[clojure.pprint :refer [pprint]]
[clojure.test :as t]
[mockery.core :refer [with-mocks]]))
@@ -290,3 +291,289 @@
(th/create-global-complaint-for pool {:type :bounce :email (:email profile)})
(t/is (true? (email/has-bounce-reports? pool (:email profile))))))
(t/deftest test-validate-sns-url-rejects-s3-and-other-services
;; S3 buckets are attacker-controlled
(t/is (false? (#'awsns/valid-sns-url? "https://my-bucket.s3.amazonaws.com/cert.pem")))
(t/is (false? (#'awsns/valid-sns-url? "https://my-bucket.s3.eu-central-1.amazonaws.com/cert.pem")))
;; Other AWS services
(t/is (false? (#'awsns/valid-sns-url? "https://lambda.amazonaws.com/cert.pem")))
(t/is (false? (#'awsns/valid-sns-url? "https://ec2.amazonaws.com/cert.pem")))
;; Plain amazonaws.com without sns prefix
(t/is (false? (#'awsns/valid-sns-url? "https://amazonaws.com/cert.pem"))))
(t/deftest test-validate-sns-url-accepts-only-sns-hosts
;; Valid SNS URLs with region
(t/is (true? (#'awsns/valid-sns-url? "https://sns.eu-central-1.amazonaws.com/cert.pem")))
(t/is (true? (#'awsns/valid-sns-url? "https://sns.us-east-1.amazonaws.com/cert.pem")))
(t/is (true? (#'awsns/valid-sns-url? "https://sns.ap-southeast-1.amazonaws.com/cert.pem"))))
;; Helper to load test certificate and private key from resources
;; See: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html
(defn- load-test-cert-and-key
"Loads the test certificate and private key from test resources."
[]
(let [cert-pem (slurp (clojure.java.io/resource "sns-test-cert.pem"))
key-pem (slurp (clojure.java.io/resource "sns-test-key.pem"))
;; Parse certificate
cert-bytes (.getBytes (-> cert-pem
(clojure.string/replace "-----BEGIN CERTIFICATE-----" "")
(clojure.string/replace "-----END CERTIFICATE-----" "")
(clojure.string/replace #"\s+" ""))
java.nio.charset.StandardCharsets/UTF_8)
cert-input (java.io.ByteArrayInputStream. (.decode (java.util.Base64/getDecoder) cert-bytes))
cf (java.security.cert.CertificateFactory/getInstance "X.509")
cert (.generateCertificate cf cert-input)
;; Parse private key
key-bytes (.getBytes (-> key-pem
(clojure.string/replace "-----BEGIN PRIVATE KEY-----" "")
(clojure.string/replace "-----END PRIVATE KEY-----" "")
(clojure.string/replace #"\s+" ""))
java.nio.charset.StandardCharsets/UTF_8)
key-spec (java.security.spec.PKCS8EncodedKeySpec. (.decode (java.util.Base64/getDecoder) key-bytes))
kf (java.security.KeyFactory/getInstance "RSA")
private-key (.generatePrivate kf key-spec)]
{:cert cert
:cert-bytes (.getEncoded cert)
:private-key private-key
:public-key (.getPublicKey cert)}))
(t/deftest test-verify-signature-end-to-end-v1
(let [{:keys [cert-bytes private-key]} (load-test-cert-and-key)
msg {"Type" "Notification"
"MessageId" "test-msg-1"
"TopicArn" "arn:aws:sns:us-east-1:123:topic"
"Message" "test message"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"}
string-to-sign (#'awsns/build-string-to-sign msg)
sig (java.security.Signature/getInstance "SHA1withRSA")
_ (.initSign sig private-key)
_ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8))
signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig))
msg-with-sig (assoc msg "Signature" signature)]
(with-redefs [awsns/fetch-certificate (fn [_ _]
(java.io.ByteArrayInputStream. cert-bytes))]
(t/is (true? (#'awsns/verify-signature {} msg-with-sig))))))
(t/deftest test-verify-signature-end-to-end-v2
(let [{:keys [cert-bytes private-key]} (load-test-cert-and-key)
msg {"Type" "Notification"
"MessageId" "test-msg-2"
"TopicArn" "arn:aws:sns:us-east-1:123:topic"
"Message" "test message"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem"
"SignatureVersion" "2"}
string-to-sign (#'awsns/build-string-to-sign msg)
sig (java.security.Signature/getInstance "SHA256withRSA")
_ (.initSign sig private-key)
_ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8))
signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig))
msg-with-sig (assoc msg "Signature" signature)]
(with-redefs [awsns/fetch-certificate (fn [_ _]
(java.io.ByteArrayInputStream. cert-bytes))]
(t/is (true? (#'awsns/verify-signature {} msg-with-sig))))))
(t/deftest test-verify-signature-end-to-end-subscription-confirmation
(let [{:keys [cert-bytes private-key]} (load-test-cert-and-key)
msg {"Type" "SubscriptionConfirmation"
"MessageId" "test-msg-3"
"TopicArn" "arn:aws:sns:us-east-1:123:topic"
"Message" "You have chosen to subscribe"
"Timestamp" "2021-02-04T14:41:37.020Z"
"Token" "test-token-123"
"SubscribeURL" "https://sns.us-east-1.amazonaws.com/confirm"
"SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"}
string-to-sign (#'awsns/build-string-to-sign msg)
sig (java.security.Signature/getInstance "SHA1withRSA")
_ (.initSign sig private-key)
_ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8))
signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig))
msg-with-sig (assoc msg "Signature" signature)]
(with-redefs [awsns/fetch-certificate (fn [_ _]
(java.io.ByteArrayInputStream. cert-bytes))]
(t/is (true? (#'awsns/verify-signature {} msg-with-sig))))))
(t/deftest test-verify-signature-rejects-wrong-key
(let [{:keys [cert-bytes]} (load-test-cert-and-key)
;; Generate a different keypair for signing
keypair-gen (java.security.KeyPairGenerator/getInstance "RSA")
_ (.initialize keypair-gen 2048)
kp (.generateKeyPair keypair-gen)
wrong-private-key (.getPrivate kp)
msg {"Type" "Notification"
"MessageId" "test-msg-4"
"TopicArn" "arn:aws:sns:us-east-1:123:topic"
"Message" "test message"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"}
string-to-sign (#'awsns/build-string-to-sign msg)
sig (java.security.Signature/getInstance "SHA1withRSA")
_ (.initSign sig wrong-private-key)
_ (.update sig (.getBytes string-to-sign java.nio.charset.StandardCharsets/UTF_8))
signature (.encodeToString (java.util.Base64/getEncoder) (.sign sig))
msg-with-sig (assoc msg "Signature" signature)]
(with-redefs [awsns/fetch-certificate (fn [_ _]
(java.io.ByteArrayInputStream. cert-bytes))]
(t/is (false? (#'awsns/verify-signature {} msg-with-sig))))))
(t/deftest test-verify-signature-rejects-unsupported-version
(let [msg {"Type" "Notification"
"MessageId" "test-msg-3"
"TopicArn" "arn:aws:sns:us-east-1:123:topic"
"Message" "test message"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.us-east-1.amazonaws.com/cert.pem"
"SignatureVersion" "3"
"Signature" "fake=="}]
(t/is (thrown? clojure.lang.ExceptionInfo
(#'awsns/verify-signature {} msg)))))
(t/deftest test-build-string-to-sign-v1-notification
(let [msg {"Type" "Notification"
"MessageId" "msg-123"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "{\"notificationType\":\"Bounce\"}"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"
"Signature" "abc123=="}
result (#'awsns/build-string-to-sign msg)]
(t/is (string? result))
(t/is (.contains result "MessageId"))
(t/is (.contains result "msg-123"))
(t/is (.contains result "TopicArn"))
(t/is (.contains result "Message"))
(t/is (.contains result "Timestamp"))
;; V1 does NOT include SigningCertURL, SignatureVersion, or Signature
(t/is (not (.contains result "SigningCertURL")))
(t/is (not (.contains result "SignatureVersion")))
(t/is (not (.contains result "Signature")))))
(t/deftest test-build-string-to-sign-v2-notification
(let [msg {"Type" "Notification"
"MessageId" "msg-123"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "{\"notificationType\":\"Bounce\"}"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem"
"SignatureVersion" "2"
"Signature" "abc123=="}
result (#'awsns/build-string-to-sign msg)]
(t/is (string? result))
(t/is (.contains result "MessageId"))
(t/is (.contains result "TopicArn"))
;; V2 uses the same fields as V1 (only hash algorithm differs: SHA1 vs SHA256)
;; SigningCertURL and SignatureVersion are metadata, not part of the signed content
(t/is (not (.contains result "SigningCertURL")))
(t/is (not (.contains result "SignatureVersion")))
;; Signature is never part of the string-to-sign
(t/is (not (.contains result "Signature\n")))))
(t/deftest test-build-string-to-sign-subscription-confirmation
(let [msg {"Type" "SubscriptionConfirmation"
"MessageId" "msg-456"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "You have chosen to subscribe"
"Timestamp" "2021-02-04T14:41:37.020Z"
"Token" "test-token-123"
"SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"
"Signature" "xyz789=="
"SubscribeURL" "https://sns.eu-central-1.amazonaws.com/confirm"}
result (#'awsns/build-string-to-sign msg)]
(t/is (string? result))
(t/is (.contains result "SubscribeURL"))
(t/is (.contains result "https://sns.eu-central-1.amazonaws.com/confirm"))
;; Token must be included for SubscriptionConfirmation
(t/is (.contains result "Token"))
(t/is (.contains result "test-token-123"))))
(t/deftest test-handle-request-returns-4xx-for-invalid-signature
(let [{:keys [cert-bytes]} (load-test-cert-and-key)
body (j/write-str
{"Type" "Notification"
"MessageId" "msg-123"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "{\"test\":\"data\"}"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"
"Signature" "invalid-signature=="})
result (with-redefs [awsns/fetch-certificate (fn [_ _]
(java.io.ByteArrayInputStream. cert-bytes))]
(#'awsns/handle-request th/*system* body))]
(t/is (= 400 (:status result)))))
(t/deftest test-handle-request-returns-4xx-for-invalid-url
(let [body (j/write-str
{"Type" "Notification"
"MessageId" "msg-123"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "{\"test\":\"data\"}"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://evil.com/cert.pem"
"SignatureVersion" "1"
"Signature" "fake-signature=="})
result (#'awsns/handle-request th/*system* body)]
(t/is (= 400 (:status result)))))
(t/deftest test-handle-request-rejects-invalid-signing-cert-url
(let [pool (:app.db/pool th/*system*)
profile (th/create-profile* 1)
token (tokens/generate th/*system*
{:iss :profile-identity
:profile-id (:id profile)})
body (j/write-str
{"Type" "Notification"
"MessageId" "msg-123"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" (j/write-str {"notificationType" "Bounce"
"bounce" {"bounceType" "Permanent"
"bounceSubType" "General"
"bouncedRecipients" [{"emailAddress" "victim@example.com"}]
"timestamp" "2021-02-04T14:41:38.000Z"}
"mail" {"source" "no-reply@penpot.app"
"destination" ["victim@example.com"]
"timestamp" "2021-02-04T14:41:37.020Z"
"headers" [{"name" "X-Penpot-Data" "value" token}]}})
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://evil.com/cert.pem"
"SignatureVersion" "1"
"Signature" "fake-signature=="})]
(#'awsns/handle-request th/*system* body)
(let [reports (db/query pool :global-complaint-report :all)]
(t/is (empty? reports)))))
(t/deftest test-handle-request-rejects-invalid-subscribe-url
(let [pool (:app.db/pool th/*system*)
body (j/write-str
{"Type" "SubscriptionConfirmation"
"MessageId" "msg-456"
"TopicArn" "arn:aws:sns:eu-central-1:123:topic"
"Message" "You have chosen to subscribe"
"Timestamp" "2021-02-04T14:41:37.020Z"
"SigningCertURL" "https://sns.eu-central-1.amazonaws.com/cert.pem"
"SignatureVersion" "1"
"Signature" "fake-signature=="
"SubscribeURL" "http://attacker.com/confirm"})]
(#'awsns/handle-request th/*system* body)
(let [reports (db/query pool :global-complaint-report :all)]
(t/is (empty? reports)))))
+19
View File
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDGDCCAgCgAwIBAgIJAMp8qfcfOi+AMA0GCSqGSIb3DQEBDAUAMDoxCzAJBgNV
BAYTAlVTMQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQDEwRU
ZXN0MB4XDTI2MDgwNTE1NTQxN1oXDTI3MDgwNTE1NTQxN1owOjELMAkGA1UEBhMC
VVMxDTALBgNVBAcTBFRlc3QxDTALBgNVBAoTBFRlc3QxDTALBgNVBAMTBFRlc3Qw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDj18ep0Pm5Y8VdXOSH9kXv
7RWzmxfNvN/zD1Fp2scADGBXnuVUhHYoR09Pe8U26X2gmDRg+JS7h6XXcGaWGUyh
28HISH3J0XrnKQmDKO77kxWFOAeu5apP2NVv1Nndbv+VqQczZqNZ6Kq9juflHjfK
hnnYiQqKZZ118IMCnu0uX26ShTwZJe7rAtO4MB4UNnfmAeRdrBOdI+1fp7QItYRc
2ppK3pXo2MAh8WhFeUy2UrlW6w1FpRjG/8nMmr14mD/dP9aWW85vcht5sA19bU3I
sf8ZOxOO9SGAk8Tg5Ey8aCdL5BU05OF8PNClOuqhHdGKShul3n2Pgojb2yGBHTYB
AgMBAAGjITAfMB0GA1UdDgQWBBTQbRwLjgbx/S7j1S/QFNB5HakDEjANBgkqhkiG
9w0BAQwFAAOCAQEAklxu68LkEdTFthA/ASyzm7KNxiWw+k+9IaEcViuwVa3XR2Ly
cO3L00NznsctwtcbevIT6ZlaK454snnpc5qZWHZSTYjQP34svT+XUZkgT29JdiYe
r5so+cUdorJyngMAen/tzpAHXV2l9SbRsi5+fRkR8DjPv9Ctz+kmh/Oy5fF3x/nu
AVZiwO8Oq4M3r+ElEfFed+9MkIQ6OZ3+ezgRhJYlXzVzii1OYQYpndocGNB+Jgwy
1FqzP1/02dzT6Oz95Md3B9fxj0OSCpCCBMo3ihkOKuNixaqyQ2+n/06Y+qpvnfuM
YrXnhbdpH7hsJM0/xNjWJMQlP04+zr1CQ9V9tg==
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDj18ep0Pm5Y8Vd
XOSH9kXv7RWzmxfNvN/zD1Fp2scADGBXnuVUhHYoR09Pe8U26X2gmDRg+JS7h6XX
cGaWGUyh28HISH3J0XrnKQmDKO77kxWFOAeu5apP2NVv1Nndbv+VqQczZqNZ6Kq9
juflHjfKhnnYiQqKZZ118IMCnu0uX26ShTwZJe7rAtO4MB4UNnfmAeRdrBOdI+1f
p7QItYRc2ppK3pXo2MAh8WhFeUy2UrlW6w1FpRjG/8nMmr14mD/dP9aWW85vcht5
sA19bU3Isf8ZOxOO9SGAk8Tg5Ey8aCdL5BU05OF8PNClOuqhHdGKShul3n2Pgojb
2yGBHTYBAgMBAAECggEAIPku65wTL+nI+9iAOE8DUxQoIlyNJtixPl9WpG+lghPI
c5XK0Z7z7KNZToL2iRpkdHPijLAc8kDQ1uts5UcXCIuhsUcQcT8wPrj5J/KqF11z
bVqs/fo92h1i0jLnLr0sHvAd2yn89PuPjixa0hU79MLeamB21o2bKqDajOwMHjwq
bAFUED3rmQe0nLv1pJbCIUUqd4ZElx+/XV+Y0c329zfA0WzjJI2EkHCnsS4tQn50
KX77Mzcbm9ujVQf2xsr5pnRscsCk2LrzXmAW4nOw5jpeWxxudvVTzARhpv46mj0j
MN0EN00YSw9FAHXBwDrjzS6cvvkchmBTJCUpKcoEWQKBgQDmO23YlsHrtnpWZRKf
BTu/uIObLHXgNt1yXby0mFk2BSe6L6yJDxalJQ1jxdyay73+LLBUdJwfg2K/jRdw
PFDdOvSRpb42LxTJ0TQxNaXsr91FDmbkjj0rFk1ygZKd/79HZq7xKWsfHsyTfUI7
wgYz9QaapYYM1t1+diXlWxupJQKBgQD9V+T0EsuOTof8goKcBNw6C8zPnif8mKwy
qtFWVySWpcreq74Q9+qyhMzNKDQ2flb1Akc6J3CSSvZBR21IKuqk9Cgu3qMH5CrX
pe8smw8LpPaO5adnHvRgPTih7tJ0u7jUErWhmavkqroEq6DmvngmQ/6w9nhwTaC4
tWfJzPvIrQKBgQDA19qEZpJ7y1bhcruMMyf+yKCDo1QAwDPwjY94fXuMAflqvG/6
RYckQMrcXWkQx8OWWPxBYYM76iMWaynMutjI1Y7xSDDw1bLF8NOUvGkEvbHLG+sX
WgTmSEIKvXl/mi4vslSqb5TodjXI/Ew0HapwbrZfZnHH41mXiYLof83FeQKBgQDK
91T1ee1c6GuoEINFLdumIXgHyeStST+EJDgsXQpyKwd6F8vhWk3MkfpmTtRt6BAQ
oK+h1qEogygBKpFR5Rgx6W4cBsBEfTcZp9YTPXLzWEk0OKdCRZlxVPr/OQ+g+Bhe
x1J+0lfVjjYTsdDprCUkOwtciUn6ZybhdGxfT3tUzQKBgQC2BxPNlzPJvRkLvfMJ
fJXfM9g96Uy7to7a7+LcLB2+OEAt/5kQAMg6ishwwoNvxVMtdy5uLEYNpq74lnQ2
bCVc7qc834edVMykhjlK5CLu0GiK2NM4aQavBNRGBoLF7I1Ih+tcToZFUC6W8YEU
djPrILOfRncjBj+epHXo92Z0iA==
-----END PRIVATE KEY-----