From 5e755fff9d70a7fd3c4f85bb524f1b774dd85b25 Mon Sep 17 00:00:00 2001
From: Rigel Kent <par@rigelk.eu>
Date: Thu, 13 Dec 2018 09:49:45 +0100
Subject: [PATCH] add Content Security Policy (#1252)

* add Content Security Policy

* remove reflect-metadata on production builds to get rid of unsafe-eval

* fix baseCSP usage

* add SRI to CSP

* add blob: to media-src

* remove SRI

* CSP set to reportOnly

* adding data: to connect-src CSP

* remove block-all-mixed-content

* add report-uri support
---
 client/src/environments/environment.ts        |  7 +++
 client/src/polyfills.ts                       |  8 +++-
 config/default.yaml                           |  2 +
 config/production.yaml.example                |  2 +
 server.ts                                     |  3 ++
 server/controllers/client.ts                  |  3 +-
 server/initializers/constants.ts              |  1 +
 server/middlewares/csp.ts                     | 45 +++++++++++++++++++
 server/middlewares/dnt.ts                     |  2 +-
 server/middlewares/index.ts                   |  2 +
 .../config/custom-environment-variables.yaml  |  4 +-
 11 files changed, 75 insertions(+), 4 deletions(-)
 create mode 100644 server/middlewares/csp.ts

diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts
index 5bb6f4b344..1ea4835548 100644
--- a/client/src/environments/environment.ts
+++ b/client/src/environments/environment.ts
@@ -2,6 +2,13 @@
 // `ng build --env=prod` then `environment.prod.ts` will be used instead.
 // The list of which env maps to which file can be found in `.angular-cli.json`.
 
+// Reflect.metadata polyfill is only needed in the JIT/dev mode.
+//
+// In order to load these polyfills early enough (before app code), polyfill.ts imports this file to
+// to change the order in the final bundle.
+import 'core-js/es6/reflect'
+import 'core-js/es7/reflect'
+
 export const environment = {
   production: false,
   hmr: false,
diff --git a/client/src/polyfills.ts b/client/src/polyfills.ts
index 5dff18632b..3689084324 100644
--- a/client/src/polyfills.ts
+++ b/client/src/polyfills.ts
@@ -45,7 +45,13 @@ import 'core-js/es7/object'
 /** IE10 and IE11 requires the following for the Reflect API. */
 
 // For Google Bot
-import 'core-js/es6/reflect'
+// import 'core-js/es6/reflect'; // --> dealt with in src/environment.ts
+
+/**
+ * Evergreen browsers require these.
+ */
+// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
+// import 'core-js/es7/reflect' // --> dealt with in src/environment.ts
 
 /**
  * Required to support Web Animations `@angular/platform-browser/animations`.
diff --git a/config/default.yaml b/config/default.yaml
index 080638a133..5fdb41250f 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -163,6 +163,8 @@ instance:
     "# If you would like to report a security issue\n# you may report it to:\nContact: https://github.com/Chocobozzz/PeerTube/blob/develop/SECURITY.md\nContact: mailto:"
 
 services:
+  # You can provide a reporting endpoint for Content Security Policy violations
+  csp-logger:
   # Cards configuration to format video in Twitter
   twitter:
     username: '@Chocobozzz' # Indicates the Twitter account for the website or platform on which the content was published
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 770bb97da7..c0dbf64b61 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -177,6 +177,8 @@ instance:
     "# If you would like to report a security issue\n# you may report it to:\nContact: https://github.com/Chocobozzz/PeerTube/blob/develop/SECURITY.md\nContact: mailto:"
 
 services:
+  # You can provide a reporting endpoint for Content Security Policy violations
+  csp-logger:
   # Cards configuration to format video in Twitter
   twitter:
     username: '@Chocobozzz' # Indicates the Twitter account for the website or platform on which the content was published
diff --git a/server.ts b/server.ts
index 4a2a6ddf55..6dff16f462 100644
--- a/server.ts
+++ b/server.ts
@@ -53,6 +53,9 @@ if (errorMessage !== null) {
 app.set('trust proxy', CONFIG.TRUST_PROXY)
 
 // Security middleware
+import { baseCSP } from './server/middlewares'
+
+app.use(baseCSP)
 app.use(helmet({
   frameguard: {
     action: 'deny' // we only allow it for /videos/embed, see server/controllers/client.ts
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 73b40cf651..e5bd487f14 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
 import { join } from 'path'
 import { root } from '../helpers/core-utils'
 import { ACCEPT_HEADERS, STATIC_MAX_AGE } from '../initializers'
-import { asyncMiddleware } from '../middlewares'
+import { asyncMiddleware, embedCSP } from '../middlewares'
 import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '../../shared/models/i18n/i18n'
 import { ClientHtml } from '../lib/client-html'
 import { logger } from '../helpers/logger'
@@ -22,6 +22,7 @@ clientsRouter.use('/videos/watch/:id',
 
 clientsRouter.use('' +
   '/videos/embed',
+  embedCSP,
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     res.removeHeader('X-Frame-Options')
     res.sendFile(embedPath)
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index ad61bee738..f1a734f48c 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -290,6 +290,7 @@ const CONFIG = {
     get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') }
   },
   SERVICES: {
+    get 'CSP-LOGGER' () { return config.get<string>('services.csp-logger') },
     TWITTER: {
       get USERNAME () { return config.get<string>('services.twitter.username') },
       get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
new file mode 100644
index 0000000000..a0ed3710b5
--- /dev/null
+++ b/server/middlewares/csp.ts
@@ -0,0 +1,45 @@
+import * as helmet from 'helmet'
+import { CONFIG } from '../initializers/constants'
+
+const baseDirectives = Object.assign({},
+  {
+    defaultSrc: ["'none'"], // by default, not specifying default-src = '*'
+    connectSrc: ['*', 'data:'],
+    mediaSrc: ["'self'", 'https:', 'blob:'],
+    fontSrc: ["'self'", 'data:'],
+    imgSrc: ["'self'", 'data:'],
+    scriptSrc: ["'self' 'unsafe-inline'"],
+    styleSrc: ["'self' 'unsafe-inline'"],
+    // objectSrc: ["'none'"], // only define to allow plugins, else let defaultSrc 'none' block it
+    formAction: ["'self'"],
+    frameAncestors: ["'none'"],
+    baseUri: ["'self'"],
+    pluginTypes: ["'none'"],
+    manifestSrc: ["'self'"],
+    frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed
+    workerSrc: ["'self'"], // instead of deprecated child-src
+    upgradeInsecureRequests: true
+  },
+  (CONFIG.SERVICES['CSP-LOGGER'] != null) ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}
+)
+
+const baseCSP = helmet.contentSecurityPolicy({
+  directives: baseDirectives,
+  browserSniff: false,
+  reportOnly: true
+})
+
+const embedCSP = helmet.contentSecurityPolicy({
+  directives: Object.assign(baseDirectives, {
+    frameAncestors: ['*']
+  }),
+  browserSniff: false, // assumes a modern browser, but allows CDN in front
+  reportOnly: true
+})
+
+// ---------------------------------------------------------------------------
+
+export {
+  baseCSP,
+  embedCSP
+}
diff --git a/server/middlewares/dnt.ts b/server/middlewares/dnt.ts
index cabad39c6c..607def8555 100644
--- a/server/middlewares/dnt.ts
+++ b/server/middlewares/dnt.ts
@@ -10,4 +10,4 @@ const advertiseDoNotTrack = (_, res, next) => {
 
 export {
   advertiseDoNotTrack
- }
+}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index 0cef26953d..b758a8586b 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -6,3 +6,5 @@ export * from './pagination'
 export * from './servers'
 export * from './sort'
 export * from './user-right'
+export * from './dnt'
+export * from './csp'
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml
index cfc30632cb..550f1ad80c 100644
--- a/support/docker/production/config/custom-environment-variables.yaml
+++ b/support/docker/production/config/custom-environment-variables.yaml
@@ -101,9 +101,11 @@ transcoding:
     1080:
       __name: "PEERTUBE_TRANSCODING_1080P"
       __format: "json"
-    
 
 instance:
   name: "PEERTUBE_INSTANCE_NAME"
   description: "PEERTUBE_INSTANCE_DESCRIPTION"
   terms: "PEERTUBE_INSTANCE_TERMS"
+
+services:
+  csp-logger: "PEERTUBE_SERVICES_CSPLOGGER"
-- 
GitLab