Skip to content
Snippets Groups Projects
Unverified Commit 6c660631 authored by Cam Saul's avatar Cam Saul
Browse files

Remove unsafe-inline directive from CSP :lock:

parent 9c8b0431
No related branches found
No related tags found
No related merge requests found
......@@ -16,23 +16,34 @@
<meta name="msapplication-config" content="/app/assets/img/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="base-href" content="{{{baseHref}}}" />
<meta name="uri" content="{{{uri}}}" />
{{{embed_code}}}
{{{embedCode}}}
<title>Metabase</title>
<base href={{{base_href}}} />
<base href="{{{baseHref}}}" />
<script type="application/json" id="_metabaseBootstrap">
{{{bootstrapJSON}}}
</script>
<script type="application/json" id="_metabaseLocalization">
{{{localizationJSON}}}
</script>
<!-- If you modify this script, make sure you update the whitelisted Content-Security-Policy hash in metabase.middleware.security -->
<script type="text/javascript">
(function() {
window.MetabaseBootstrap = {{{bootstrap_json}}};
window.MetabaseLocalization = {{{localization_json}}};
window.MetabaseBootstrap = JSON.parse(document.getElementById("_metabaseBootstrap").innerHTML);
window.MetabaseLocalization = JSON.parse(document.getElementById("_metabaseLocalization").innerHTML);
var configuredRoot = {{{base_href}}};
var configuredRoot = document.head.querySelector("meta[name='base-href']").content;
var actualRoot = "/";
// Add trailing slashes
var backendPathname = {{{uri}}}.replace(/\/*$/, "/");
var backendPathname = document.head.querySelector("meta[name='uri']").content.replace(/\/*$/, "/");
// e.x. "/questions/"
var frontendPathname = window.location.pathname.replace(/\/*$/, "/");
// e.x. "/metabase/questions/"
......@@ -56,42 +67,39 @@
<body>
<div id="root"></div>
<!-- Using the Web Font Loader lets us load the fonts asynchronously for faster page loads -- see https://github.com/typekit/webfontloader -->
<!-- If you modify this script, make sure you update the whitelisted Content-Security-Policy hash in metabase.middleware.security -->
<script type="text/javascript">
// Load scripts asyncronously after the page has finished loading
(function () {
function loadScript(src, onload) {
var script = document.createElement('script');
script.type = "text/javascript";
script.async = true;
script.src = src;
if (onload) script.onload = onload;
document.body.appendChild(script);
}
loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () {
WebFont.load({ google: {
families: ["Lato:300,400,700,900"] }
});
});
var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id;
if (googleAuthClientID) {
loadScript('https://apis.google.com/js/api:client.js');
WebFontConfig = {
google: {
families: ["Lato:300,400,700,900"]
}
})();
};
</script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js" async></script>
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
// if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview
const tracking = window.MetabaseBootstrap.anon_tracking_enabled;
const ga_code = window.MetabaseBootstrap.ga_code;
if (!tracking) {
window['ga-disable-'+ga_code] = true;
}
ga('create', ga_code, 'auto');
</script>
<!-- If Google Auth is enabled load the Google Auth JS lib -->
{{#enableGoogleAuth}}
<script type="text/javascript" src="https://apis.google.com/js/api:client.js" async></script>
{{/enableGoogleAuth}}
<!-- Google Analytics -->
<!-- If you modify this script, make sure you update the whitelisted Content-Security-Policy hash in metabase.middleware.security -->
{{#enableAnonTracking}}
<script type="text/javascript">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
// if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview
const tracking = window.MetabaseBootstrap.anon_tracking_enabled;
const ga_code = window.MetabaseBootstrap.ga_code;
if (!tracking) {
window['ga-disable-'+ga_code] = true;
}
ga('create', ga_code, 'auto');
</script>
{{/enableAnonTracking}}
</body>
</html>
(ns metabase.handler
"Top-level Metabase Ring handler."
(:require [metabase.middleware
(:require [metabase
[config :as config]
[routes :as routes]]
[metabase.middleware
[auth :as mw.auth]
[exceptions :as mw.exceptions]
[json :as mw.json]
......@@ -8,7 +11,6 @@
[misc :as mw.misc]
[security :as mw.security]
[session :as mw.session]]
[metabase.routes :as routes]
[ring.middleware
[cookies :refer [wrap-cookies]]
[keyword-params :refer [wrap-keyword-params]]
......@@ -22,7 +24,11 @@
"The primary entry point to the Ring HTTP server."
;; ▼▼▼ POST-PROCESSING ▼▼▼ happens from TOP-TO-BOTTOM
(->
#'routes/routes ; the #' is to allow tests to redefine endpoints
;; when running TESTS use the var so we can redefine routes as needed. No need to waste time with repetitive var
;; lookups when running normally
(if config/is-test?
#'routes/routes
routes/routes)
mw.exceptions/catch-uncaught-exceptions ; catch any Exceptions that weren't passed to `raise`
mw.exceptions/catch-api-exceptions ; catch exceptions and return them in our expected format
mw.log/log-api-call
......
......@@ -29,35 +29,43 @@
"`Content-Security-Policy` header. See https://content-security-policy.com for more details."
{"Content-Security-Policy"
(str/join
(for [[k vs] {:default-src ["'none'"]
:script-src ["'unsafe-inline'"
"'unsafe-eval'"
"'self'"
"https://maps.google.com"
"https://apis.google.com"
"https://www.google-analytics.com" ; Safari requires the protocol
"https://*.googleapis.com"
"*.gstatic.com"
(when config/is-dev?
"localhost:8080")]
:child-src ["'self'"
;; TODO - double check that we actually need this for Google Auth
"https://accounts.google.com"]
:style-src ["'unsafe-inline'"
"'self'"
"fonts.googleapis.com"]
:font-src ["'self'"
"fonts.gstatic.com"
"themes.googleusercontent.com"
(when config/is-dev?
"localhost:8080")]
:img-src ["*"
"'self' data:"]
:connect-src ["'self'"
"metabase.us10.list-manage.com"
(when config/is-dev?
"localhost:8080 ws://localhost:8080")]}]
(format "%s %s; " (name k) (apply str (interpose " " vs)))))})
(for [[k vs] {:default-src ["'none'"]
:script-src ["'self'"
"'unsafe-eval'" ; TODO - we keep working towards removing this entirely
"https://maps.google.com"
"https://apis.google.com"
"https://www.google-analytics.com" ; Safari requires the protocol
"https://*.googleapis.com"
"*.gstatic.com"
;; for webpack hot reloading
(when config/is-dev?
"localhost:8080")
;; inline script in index.html that sets `MetabaseBootstrap` and the like
"'sha256-dPkijyoRrGWwHu+PrDIMt/5wQPFfVuvhU7vendgcUSQ='"
;; Web Font Loader font configuration (WebFontConfig) in index.html
"'sha256-6xC9z5Dcryu9jbxUZkBJ5yUmSofhJjt7Mbnp/ijPkFs='"
;; inline script in index.html that loads Google Analytics
"'sha256-uKEj/Qp9AmQA2Xv83bZX9mNVV2VWZteZjIsVNVzLkA0='"]
:child-src ["'self'"
;; TODO - double check that we actually need this for Google Auth
"https://accounts.google.com"]
:style-src ["'self'"
"'unsafe-inline'" ; needed for Google Fonts
"fonts.googleapis.com"]
:font-src ["'self'"
"fonts.gstatic.com"
"themes.googleusercontent.com"
(when config/is-dev?
"localhost:8080")]
:img-src ["*"
"'self' data:"]
:connect-src ["'self'"
;; MailChimp. So people can sign up for the Metabase mailing list in the sign up process
"metabase.us10.list-manage.com"
(when config/is-dev?
"localhost:8080 ws://localhost:8080")]
:manifest-src ["'self'"]}]
(format "%s %s; " (name k) (str/join " " vs))))})
(defsetting ssl-certificate-public-key
(str (tru "Base-64 encoded public key for this site's SSL certificate.")
......@@ -65,10 +73,6 @@
(tru "See {0} for more information." "http://mzl.la/1EnfqBf")))
;; TODO - it would be nice if we could make this a proper link in the UI; consider enabling markdown parsing
#_(defn- public-key-pins-header []
(when-let [k (ssl-certificate-public-key)]
{"Public-Key-Pins" (format "pin-sha256=\"base64==%s\"; max-age=31536000" k)}))
(defn security-headers
"Fetch a map of security headers that should be added to a response based on the passed options."
[& {:keys [allow-iframes? allow-cache?]
......@@ -79,7 +83,6 @@
(cache-prevention-headers))
strict-transport-security-header
content-security-policy-header
#_(public-key-pins-header)
(when-not allow-iframes?
;; Tell browsers not to render our site as an iframe (prevent clickjacking)
{"X-Frame-Options" "DENY"})
......
......@@ -8,6 +8,7 @@
[compojure
[core :refer [context defroutes GET]]
[route :as route]]
[hiccup.util :as h.util]
[metabase
[public-settings :as public-settings]
[util :as u]]
......@@ -21,12 +22,13 @@
[stencil.core :as stencil]))
(defn- base-href []
(str (.getPath (io/as-url (public-settings/site-url))) "/"))
(let [path (some-> (public-settings/site-url) io/as-url .getPath)]
(str path "/")))
(defn- escape-script [s]
;; Escapes text to be included in an inline <script> tag, in particular the string '</script'
;; https://stackoverflow.com/questions/14780858/escape-in-script-tag-contents/23983448#23983448
(str/replace s #"</script" "</scr\\\\ipt"))
(str/replace s #"(?i)</script" "</scr\\\\ipt"))
(defn- load-file-at-path [path]
(slurp (or (io/resource path)
......@@ -37,10 +39,14 @@
(defn- fallback-localization
[locale]
(json/generate-string {"headers" {"language" locale
"plural-forms" "nplurals=2; plural=(n != 1);"}
"translations" {"" {"Metabase" {"msgid" "Metabase"
"msgstr" ["Metabase"]}}}}))
(json/generate-string
{"headers"
{"language" locale
"plural-forms" "nplurals=2; plural=(n != 1);"}
"translations"
{"" {"Metabase" {"msgid" "Metabase"
"msgstr" ["Metabase"]}}}}))
(defn- load-localization []
(if (and *locale* (not= (str *locale*) "en"))
......@@ -54,11 +60,14 @@
(defn- load-entrypoint-template [entrypoint-name embeddable? uri]
(load-template
(str "frontend_client/" entrypoint-name ".html")
{:bootstrap_json (escape-script (json/generate-string (public-settings/public-settings)))
:localization_json (escape-script (load-localization))
:uri (escape-script (json/generate-string uri))
:base_href (escape-script (json/generate-string (base-href)))
:embed_code (when embeddable? (embed/head uri))}))
(let [{:keys [anon_tracking_enabled google_auth_client_id], :as public-settings} (public-settings/public-settings)]
{:bootstrapJSON (escape-script (json/generate-string public-settings))
:localizationJSON (escape-script (load-localization))
:uri (h.util/escape-html uri)
:baseHref (h.util/escape-html (base-href))
:embedCode (when embeddable? (embed/head uri))
:enableGoogleAuth (boolean google_auth_client_id)
:enableAnonTracking (boolean anon_tracking_enabled)})))
(defn- entrypoint
"Repsonse that serves up an entrypoint into the Metabase application, e.g. `index.html`."
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment