mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 11:54:36 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64fe8e57e3 | ||
|
|
a4b9ccee8b | ||
|
|
2101d657e5 | ||
|
|
a8a441b7ad |
No files matched your search
+1
-1
@@ -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
@@ -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)))))
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
Reference in new issue
Block a user