diff --git a/backend/src/app/media/local.clj b/backend/src/app/media/local.clj
index b53c5a5f6d..f86e46c02e 100644
--- a/backend/src/app/media/local.clj
+++ b/backend/src/app/media/local.clj
@@ -7,30 +7,22 @@
(ns app.media.local
"Local media processing via ImageMagick and FontForge shell commands."
(:require
- [app.common.data :as d]
- [app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.media :as cm]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.config :as cf]
+ [app.media.svg :as svg]
[app.media.validation :as validation]
[app.storage.tmp :as tmp]
[app.util.shell :as shell]
[buddy.core.bytes :as bb]
[buddy.core.codecs :as bc]
[clojure.string]
- [clojure.xml :as xml]
[cuerdas.core :as str]
[datoteka.fs :as fs]
- [datoteka.io :as io])
- (:import
- clojure.lang.XMLHandler
- java.io.InputStream
- javax.xml.parsers.SAXParserFactory
- javax.xml.XMLConstants
- org.apache.commons.io.IOUtils))
+ [datoteka.io :as io]))
(defmulti process (fn [_system params] (:cmd params)))
@@ -40,30 +32,6 @@
:code :not-implemented
:hint (str/fmt "No impl found for local process cmd: %s" cmd)))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; SVG PARSING
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(defn- secure-parser-factory
- [^InputStream input ^XMLHandler handler]
- (.. (doto (SAXParserFactory/newInstance)
- (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
- (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
- (newSAXParser)
- (parse input handler)))
-
-(defn- strip-doctype
- [data]
- (cond-> data
- (str/includes? data "]*>" "")))
-
-(defn parse-svg
- [text]
- (let [text (strip-doctype text)]
- (dm/with-open [istream (IOUtils/toInputStream ^String text "UTF-8")]
- (xml/parse istream secure-parser-factory))))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; IMAGE THUMBNAILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -167,34 +135,6 @@
"-extent" (str width "x" height)
"-quality" (str quality)]))))
-(defn get-basic-info-from-svg
- [{:keys [tag attrs] :as data}]
- (when (not= tag :svg)
- (ex/raise :type :validation
- :code :unable-to-parse-svg
- :hint "uploaded svg has invalid content"))
- (reduce (fn [default f]
- (if-let [res (f attrs)]
- (reduced res)
- default))
- {:width 100 :height 100}
- [(fn parse-width-and-height
- [{:keys [width height]}]
- (when (and (string? width)
- (string? height))
- (let [width (d/parse-double width)
- height (d/parse-double height)]
- (when (and width height)
- {:width (int width)
- :height (int height)}))))
- (fn parse-viewbox
- [{:keys [viewBox]}]
- (let [[x y width height] (->> (str/split viewBox #"\s+" 4)
- (map d/parse-double))]
- (when (and x y width height)
- {:width (int width)
- :height (int height)})))]))
-
(defn- get-dimensions-with-orientation [system ^String path]
;; Image magick doesn't give info about exif rotation so we use the identify command
;; If we are processing an animated gif we use the first frame with -scene 0
@@ -217,7 +157,7 @@
[system {:keys [input] :as params}]
(let [{:keys [path mtype] :as input} (validation/check-input input)]
(if (= mtype "image/svg+xml")
- (let [info (some-> path slurp parse-svg get-basic-info-from-svg)]
+ (let [info (some-> path slurp svg/parse-svg svg/get-basic-info-from-svg)]
(when-not info
(ex/raise :type :validation
:code :invalid-svg-file
diff --git a/backend/src/app/media/remote.clj b/backend/src/app/media/remote.clj
index 447d5f2e55..0b5a0a4a42 100644
--- a/backend/src/app/media/remote.clj
+++ b/backend/src/app/media/remote.clj
@@ -13,7 +13,7 @@
[app.common.uri :as uri]
[app.config :as cf]
[app.http.client :as http]
- [app.media.local :as local]
+ [app.media.svg :as svg]
[app.media.validation :as validation]
[app.setup :as-alias setup]
[app.storage.tmp :as tmp]
@@ -182,7 +182,7 @@
(let [{:keys [path mtype]} (validation/check-input input)]
(if (= mtype "image/svg+xml")
;; SVG: parse locally (Sharp doesn't support SVG)
- (let [info (some-> path slurp local/parse-svg local/get-basic-info-from-svg)]
+ (let [info (some-> path slurp svg/parse-svg svg/get-basic-info-from-svg)]
(when-not info
(ex/raise :type :validation
:code :invalid-svg-file
diff --git a/backend/src/app/media/svg.clj b/backend/src/app/media/svg.clj
new file mode 100644
index 0000000000..1de52d4030
--- /dev/null
+++ b/backend/src/app/media/svg.clj
@@ -0,0 +1,130 @@
+;; 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 Sucursal en EspaƱa SL
+
+(ns app.media.svg
+ "SVG parsing, sanitization, and info extraction.
+ Centralizes all SVG-related security concerns."
+ (:require
+ [app.common.data :as d]
+ [app.common.data.macros :as dm]
+ [app.common.exceptions :as ex]
+ [app.common.logging :as l]
+ [clojure.xml :as xml]
+ [cuerdas.core :as str])
+ (:import
+ clojure.lang.XMLHandler
+ java.io.InputStream
+ javax.xml.parsers.SAXParserFactory
+ javax.xml.XMLConstants
+ org.apache.commons.io.IOUtils))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; SVG PARSING
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- secure-parser-factory
+ [^InputStream input ^XMLHandler handler]
+ (.. (doto (SAXParserFactory/newInstance)
+ (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true)
+ (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true))
+ (newSAXParser)
+ (parse input handler)))
+
+(defn- strip-doctype
+ [data]
+ (cond-> data
+ (str/includes? data "]*>" "")))
+
+(defn parse-svg
+ [text]
+ (let [text (strip-doctype text)]
+ (dm/with-open [istream (IOUtils/toInputStream ^String text "UTF-8")]
+ (xml/parse istream secure-parser-factory))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; SVG SANITIZATION
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def ^:private dangerous-attrs-pattern #"(?i)^on\w+$")
+(def ^:private javascript-href-pattern #"(?i)^javascript:")
+
+(defn- sanitize-svg-element
+ "Recursively sanitize an SVG element by removing dangerous tags and attributes."
+ [{:keys [tag attrs content] :as element}]
+ (when (and (map? element) tag)
+ (let [dangerous-tags #{:script :foreignObject :set :animate :animateTransform :animateColor :animateMotion}]
+ (when-not (contains? dangerous-tags tag)
+ (let [clean-attrs (->> attrs
+ (remove (fn [[k v]]
+ (or (re-matches dangerous-attrs-pattern (name k))
+ (and (#{:href :xlink:href} k)
+ (string? v)
+ (re-find javascript-href-pattern (str/trim v))))))
+ (into {}))
+ clean-content (when content
+ (->> content
+ (filter #(or (string? %) (map? %)))
+ (map (fn [child]
+ (if (map? child)
+ (sanitize-svg-element child)
+ child)))
+ (filter some?)
+ vec))]
+ (cond-> {:tag tag :attrs clean-attrs}
+ (seq clean-content) (assoc :content clean-content)))))))
+
+(defn sanitize-svg
+ "Sanitize SVG content by removing dangerous elements and attributes.
+ Removes "
+ result (svg/sanitize-svg svg)]
+ (t/is (not (clojure.string/includes? result "