penpot/backend/test/backend_tests/loggers_webhooks_test.clj
Andrey Antukh 279231240d
🐛 Harden outbound HTTP requests against SSRF and restrict assets handlers (#9390)
* ⬆️ Update root deps

* 🐛 Harden outbound HTTP requests against SSRF and restrict unauthenticated asset access

- Add app.util.ssrf URL/host validator that resolves hostnames and blocks
  loopback, link-local, site-local, cloud metadata, and operator-supplied CIDRs
- Add app.media.sanitize image EOF truncator that strips trailing data after
  PNG IEND, JPEG EOI, GIF trailer, and WebP RIFF markers
- Disable HTTP client auto-redirect; add req-with-redirects! helper that
  revalidates every redirect hop against the SSRF blocklist
- Wire SSRF validation and EOF sanitization into media/download-image
- Validate webhook URLs and OIDC profile picture URLs against SSRF
- Restrict /assets/by-id to require authentication for non-public buckets
  (profile) while keeping public access for file-media-object,
  file-object-thumbnail, team-font-variant, and file-data-fragment
- Add config knobs: ssrf-protection-enabled, ssrf-allowed-hosts,
  ssrf-extra-blocked-cidrs

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 09:18:22 +02:00

108 lines
3.9 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns backend-tests.loggers-webhooks-test
(:require
[app.common.uuid :as uuid]
[app.db :as db]
[app.http :as http]
[app.storage :as sto]
[backend-tests.helpers :as th]
[clojure.test :as t]
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest process-event-handler-with-no-webhooks
(with-mocks [submit-mock {:target 'app.worker/submit! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
res (th/run-task! :process-webhook-event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}})]
(t/is (= 0 (:call-count @submit-mock)))
(t/is (nil? res)))))
(t/deftest process-event-handler
(with-mocks [submit-mock {:target 'app.worker/submit! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
whk (th/create-webhook* {:team-id (:default-team-id prof)})
res (th/run-task! :process-webhook-event
{:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}})]
(t/is (= 1 (:call-count @submit-mock)))
(t/is (nil? res)))))
(t/deftest run-webhook-handler-1
(with-mocks [http-mock {:target 'app.http.client/req :return {:status 200}}]
(let [prof (th/create-profile* 1 {:is-active true})
whk (th/create-webhook* {:team-id (:default-team-id prof)})
evt {:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}
res (th/run-task! :run-webhook
{:event evt
:config whk})]
(t/is (= 1 (:call-count @http-mock)))
(let [rows (th/db-exec! ["select * from webhook_delivery where webhook_id=?"
(:id whk)])]
(t/is (= 1 (count rows)))
(t/is (nil? (-> rows first :error-code))))
;; Refresh webhook
(let [whk' (th/db-get :webhook {:id (:id whk)})]
(t/is (nil? (:error-code whk')))))))
(t/deftest run-webhook-handler-2
(with-mocks [http-mock {:target 'app.http.client/req :return {:status 400}}]
(let [prof (th/create-profile* 1 {:is-active true})
whk (th/create-webhook* {:team-id (:default-team-id prof)})
evt {:type "command"
:name "create-project"
:props {:team-id (:default-team-id prof)}}
res (th/run-task! :run-webhook
{:event evt
:config whk})]
(t/is (= 1 (:call-count @http-mock)))
(let [rows (th/db-query :webhook-delivery {:webhook-id (:id whk)})]
(t/is (= 1 (count rows)))
(t/is (= "unexpected-status:400" (-> rows first :error-code))))
;; Refresh webhook
(let [whk' (th/db-get :webhook {:id (:id whk)})]
(t/is (= "unexpected-status:400" (:error-code whk')))
(t/is (= 1 (:error-count whk'))))
;; RUN 2 times more
(th/run-task! :run-webhook
{:event evt
:config whk})
(th/run-task! :run-webhook
{:event evt
:config whk})
(let [rows (th/db-query :webhook-delivery {:webhook-id (:id whk)})]
(t/is (= 3 (count rows)))
(t/is (= "unexpected-status:400" (-> rows first :error-code))))
;; Refresh webhook
(let [whk' (th/db-get :webhook {:id (:id whk)})]
(t/is (= "unexpected-status:400" (:error-code whk')))
(t/is (= 3 (:error-count whk')))
(t/is (false? (:is-active whk')))))))