mirror of
https://github.com/penpot/penpot.git
synced 2026-08-27 15:19:49 +00:00
🐛 Validate content-type on management upload endpoints
Add media type validation to upload-tempfile and upload-org-logo management endpoints. Both stored user-supplied mtype without checking against an allowlist. Only image types and PDF are permitted. Non-public bucket assets now also carry Content-Disposition: attachment to prevent inline rendering. AI-assisted-by: mimo-v2.5-pro
This commit is contained in:
parent
319a2185c9
commit
97e79a8194
@ -48,12 +48,16 @@
|
|||||||
[{:keys [::sto/storage ::signature-max-age ::cache-max-age] :as cfg} obj]
|
[{:keys [::sto/storage ::signature-max-age ::cache-max-age] :as cfg} obj]
|
||||||
(let [sig-max-age (or signature-max-age default-signature-max-age)
|
(let [sig-max-age (or signature-max-age default-signature-max-age)
|
||||||
cch-max-age (or cache-max-age default-cache-max-age)
|
cch-max-age (or cache-max-age default-cache-max-age)
|
||||||
{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age sig-max-age})]
|
{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age sig-max-age})
|
||||||
|
bucket (-> obj meta :bucket)
|
||||||
|
headers (cond-> {"location" (str url)
|
||||||
|
"x-host" (cond-> host port (str ":" port))
|
||||||
|
"x-mtype" (-> obj meta :content-type)
|
||||||
|
"cache-control" (str "max-age=" (inst-ms cch-max-age))}
|
||||||
|
(not (contains? public-buckets bucket))
|
||||||
|
(assoc "content-disposition" "attachment"))]
|
||||||
{::yres/status 307
|
{::yres/status 307
|
||||||
::yres/headers {"location" (str url)
|
::yres/headers headers}))
|
||||||
"x-host" (cond-> host port (str ":" port))
|
|
||||||
"x-mtype" (-> obj meta :content-type)
|
|
||||||
"cache-control" (str "max-age=" (inst-ms cch-max-age))}}))
|
|
||||||
|
|
||||||
(defn- serve-object-from-fs
|
(defn- serve-object-from-fs
|
||||||
[{:keys [::path ::cache-max-age]} obj]
|
[{:keys [::path ::cache-max-age]} obj]
|
||||||
@ -61,9 +65,12 @@
|
|||||||
purl (u/join (u/uri path)
|
purl (u/join (u/uri path)
|
||||||
(sto/object->relative-path obj))
|
(sto/object->relative-path obj))
|
||||||
mdata (meta obj)
|
mdata (meta obj)
|
||||||
headers {"x-accel-redirect" (:path purl)
|
bucket (:bucket mdata)
|
||||||
"content-type" (:content-type mdata)
|
headers (cond-> {"x-accel-redirect" (:path purl)
|
||||||
"cache-control" (str "max-age=" (inst-ms cch-max-age))}]
|
"content-type" (:content-type mdata)
|
||||||
|
"cache-control" (str "max-age=" (inst-ms cch-max-age))}
|
||||||
|
(not (contains? public-buckets bucket))
|
||||||
|
(assoc "content-disposition" "attachment"))]
|
||||||
{::yres/status 204
|
{::yres/status 204
|
||||||
::yres/headers headers}))
|
::yres/headers headers}))
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,12 @@
|
|||||||
|
|
||||||
(ns app.rpc.management.exporter
|
(ns app.rpc.management.exporter
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.media :as cm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.media :refer [schema:upload]]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.doc :as doc]
|
[app.rpc.doc :as doc]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
@ -21,7 +22,7 @@
|
|||||||
(def ^:private
|
(def ^:private
|
||||||
schema:upload-tempfile-params
|
schema:upload-tempfile-params
|
||||||
[:map {:title "upload-templfile-params"}
|
[:map {:title "upload-templfile-params"}
|
||||||
[:content schema:upload]])
|
[:content media/schema:upload]])
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:upload-tempfile-result
|
schema:upload-tempfile-result
|
||||||
@ -32,6 +33,7 @@
|
|||||||
::sm/params schema:upload-tempfile-params
|
::sm/params schema:upload-tempfile-params
|
||||||
::sm/result schema:upload-tempfile-result}
|
::sm/result schema:upload-tempfile-result}
|
||||||
[cfg {:keys [::rpc/profile-id content]}]
|
[cfg {:keys [::rpc/profile-id content]}]
|
||||||
|
(media/validate-media-type! content cm/tempfile-types)
|
||||||
(let [storage (sto/resolve cfg)
|
(let [storage (sto/resolve cfg)
|
||||||
hash (sto/calculate-hash (:path content))
|
hash (sto/calculate-hash (:path content))
|
||||||
data (-> (sto/content (:path content))
|
data (-> (sto/content (:path content))
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
[app.auth.oidc :as oidc]
|
[app.auth.oidc :as oidc]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.media :as cm]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.types.organization :as cto]
|
[app.common.types.organization :as cto]
|
||||||
@ -135,6 +136,7 @@
|
|||||||
::sm/result schema:upload-organization-logo-result
|
::sm/result schema:upload-organization-logo-result
|
||||||
::nitrate/sso false}
|
::nitrate/sso false}
|
||||||
[{:keys [::sto/storage]} {:keys [content organization-id previous-id]}]
|
[{:keys [::sto/storage]} {:keys [content organization-id previous-id]}]
|
||||||
|
(media/validate-media-type! content cm/image-types)
|
||||||
(when previous-id
|
(when previous-id
|
||||||
(sto/touch-object! storage previous-id))
|
(sto/touch-object! storage previous-id))
|
||||||
(let [hash (sto/calculate-hash (:path content))
|
(let [hash (sto/calculate-hash (:path content))
|
||||||
|
|||||||
@ -57,6 +57,22 @@
|
|||||||
(t/is (not= (get-in out1 [:result :id])
|
(t/is (not= (get-in out1 [:result :id])
|
||||||
(get-in out2 [:result :id])))))
|
(get-in out2 [:result :id])))))
|
||||||
|
|
||||||
|
(t/deftest upload-tempfile-rejects-html-content-type
|
||||||
|
;; N2-13: upload-tempfile must reject non-allowed content types
|
||||||
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
|
path (fs/create-tempfile :dir "/tmp/penpot" :prefix "test-upload-tempfile-")
|
||||||
|
_ (io/write* path "<script>alert(1)</script>")
|
||||||
|
params {::th/type :upload-tempfile
|
||||||
|
::rpc/profile-id (:id profile)
|
||||||
|
:content {:filename "evil.html"
|
||||||
|
:path path
|
||||||
|
:mtype "text/html"
|
||||||
|
:size 27}}
|
||||||
|
out (th/management-command! params)]
|
||||||
|
(t/is (some? (:error out)))
|
||||||
|
(t/is (= :validation (th/ex-type (:error out))))
|
||||||
|
(t/is (= :media-type-not-allowed (th/ex-code (:error out))))))
|
||||||
|
|
||||||
(t/deftest duplicate-file
|
(t/deftest duplicate-file
|
||||||
(let [storage (-> (:app.storage/storage th/*system*)
|
(let [storage (-> (:app.storage/storage th/*system*)
|
||||||
(configure-storage-backend))
|
(configure-storage-backend))
|
||||||
|
|||||||
@ -22,6 +22,9 @@
|
|||||||
"image/gif"
|
"image/gif"
|
||||||
"image/svg+xml"})
|
"image/svg+xml"})
|
||||||
|
|
||||||
|
(def tempfile-types
|
||||||
|
(conj image-types "application/pdf"))
|
||||||
|
|
||||||
(defn format->extension
|
(defn format->extension
|
||||||
[format]
|
[format]
|
||||||
(case format
|
(case format
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user