From 14c760aa33b5a34d24b328bc95d1c296cf1360ef Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 10 May 2024 13:24:09 -0500 Subject: [PATCH 1/6] Specify XFD and RSC urls in cors origin and csp src; remove localhost from policies; refactor csp to allow embeding DHS NTAS widget. --- backend/src/api/app.ts | 23 +++++++++++++++++------ frontend/scripts/api.js | 29 ++++++++++++++++++++++------- frontend/scripts/constants.js | 3 --- frontend/scripts/docs.js | 9 ++++++--- 4 files changed, 45 insertions(+), 19 deletions(-) delete mode 100644 frontend/scripts/constants.js diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 57f3f16f..2f5f140f 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -72,7 +72,10 @@ app.use(express.json({ strict: false })); app.use( cors({ - origin: '*', + origin: [ + /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, + /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ + ], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] }) ); @@ -83,17 +86,25 @@ app.use( directives: { defaultSrc: [ "'self'", - 'https://cognito-idp.us-east-1.amazonaws.com', - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' + 'https://cognito-idp.*.amazonaws.com', + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov' + ], + frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'], + imgSrc: [ + "'self'", + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov', + 'https://www.dhs.gov' ], objectSrc: ["'none'"], scriptSrc: [ "'self'", - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' - // Add any other allowed script sources here + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov', + 'https://www.dhs.gov' ], frameAncestors: ["'none'"] - // Add other directives as needed } }, hsts: { diff --git a/frontend/scripts/api.js b/frontend/scripts/api.js index 9dcc23d8..f4c73a33 100644 --- a/frontend/scripts/api.js +++ b/frontend/scripts/api.js @@ -5,7 +5,6 @@ import cors from 'cors'; import helmet from 'helmet'; import express from 'express'; import path from 'path'; -import { ALLOW_ORIGIN, ALLOW_METHODS } from './constants.js'; export const app = express(); @@ -17,7 +16,15 @@ app.use((req, res, next) => { next(); }); -app.use(cors({ origin: ALLOW_ORIGIN, methods: ALLOW_METHODS })); +app.use( + cors({ + origin: [ + /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, + /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ + ], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + }) +); app.use( helmet({ @@ -25,17 +32,25 @@ app.use( directives: { defaultSrc: [ "'self'", - 'https://cognito-idp.us-east-1.amazonaws.com', - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' + 'https://cognito-idp.*.amazonaws.com', + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov' + ], + frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'], + imgSrc: [ + "'self'", + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov', + 'https://www.dhs.gov' ], objectSrc: ["'none'"], scriptSrc: [ "'self'", - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' - // Add any other allowed script sources here + 'https://*.crossfeed.cyber.dhs.gov', + 'https://*.readysetcyber.cyber.dhs.gov', + 'https://www.dhs.gov' ], frameAncestors: ["'none'"] - // Add other directives as needed } }, hsts: { diff --git a/frontend/scripts/constants.js b/frontend/scripts/constants.js deleted file mode 100644 index 3520ae39..00000000 --- a/frontend/scripts/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -//CORS Options -export const ALLOW_ORIGIN = '*'; -export const ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']; diff --git a/frontend/scripts/docs.js b/frontend/scripts/docs.js index b3ef5d65..bc4c08dc 100644 --- a/frontend/scripts/docs.js +++ b/frontend/scripts/docs.js @@ -4,7 +4,6 @@ import path from 'path'; import rateLimit from 'express-rate-limit'; import cors from 'cors'; import helmet from 'helmet'; -import { ALLOW_ORIGIN, ALLOW_METHODS } from './constants.js'; export const app = express(); @@ -17,8 +16,12 @@ app.use( app.use(express.static(path.join(__dirname, '../docs/build'))); -app.use(cors({ origin: ALLOW_ORIGIN, methods: ALLOW_METHODS })); - +app.use( + cors({ + origin: [/.*crossfeed.cyber\.dhs\.gov$/], + methods: 'GET' + }) +); app.use( helmet({ contentSecurityPolicy: { From 271b63e1dffbc825fa9dbd797e368c759e9ccdb3 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 10 May 2024 14:22:31 -0500 Subject: [PATCH 2/6] Add RSC subdomains to docs cors policy; revert cors/csp in backend app.ts since it only applies to local environment. --- backend/src/api/app.ts | 29 ++++++++--------------------- frontend/scripts/docs.js | 5 ++++- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 2f5f140f..b582ac6c 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -23,8 +23,6 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; import { UserType } from '../models'; import * as assessments from './assessments'; -const sanitizer = require('sanitizer'); - if ( (process.env.IS_OFFLINE || process.env.IS_LOCAL) && typeof jest === 'undefined' @@ -50,12 +48,12 @@ const handlerToExpress = (handler) => async (req, res) => { {} ); try { - const parsedBody = JSON.parse(sanitizer.sanitize(body)); + const parsedBody = JSON.parse(body); res.status(statusCode).json(parsedBody); } catch (e) { // Not a JSON body res.setHeader('content-type', 'text/plain'); - res.status(statusCode).send(sanitizer.sanitize(body)); + res.status(statusCode).send(body); } }; @@ -72,10 +70,7 @@ app.use(express.json({ strict: false })); app.use( cors({ - origin: [ - /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, - /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ - ], + origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] }) ); @@ -86,25 +81,17 @@ app.use( directives: { defaultSrc: [ "'self'", - 'https://cognito-idp.*.amazonaws.com', - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov' - ], - frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'], - imgSrc: [ - "'self'", - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov', - 'https://www.dhs.gov' + 'https://cognito-idp.us-east-1.amazonaws.com', + 'https://api.staging-cd.crossfeed.cyber.dhs.gov' ], objectSrc: ["'none'"], scriptSrc: [ "'self'", - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov', - 'https://www.dhs.gov' + 'https://api.staging-cd.crossfeed.cyber.dhs.gov' + // Add any other allowed script sources here ], frameAncestors: ["'none'"] + // Add other directives as needed } }, hsts: { diff --git a/frontend/scripts/docs.js b/frontend/scripts/docs.js index bc4c08dc..02c60a0f 100644 --- a/frontend/scripts/docs.js +++ b/frontend/scripts/docs.js @@ -18,7 +18,10 @@ app.use(express.static(path.join(__dirname, '../docs/build'))); app.use( cors({ - origin: [/.*crossfeed.cyber\.dhs\.gov$/], + origin: [ + /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, + /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ + ], methods: 'GET' }) ); From d572471248a23909f0bc398d558b2c4d0793eee7 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 10 May 2024 14:31:29 -0500 Subject: [PATCH 3/6] Update backend/src/api/app.ts to match develop branch. --- backend/src/api/app.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index b582ac6c..57f3f16f 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -23,6 +23,8 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; import { UserType } from '../models'; import * as assessments from './assessments'; +const sanitizer = require('sanitizer'); + if ( (process.env.IS_OFFLINE || process.env.IS_LOCAL) && typeof jest === 'undefined' @@ -48,12 +50,12 @@ const handlerToExpress = (handler) => async (req, res) => { {} ); try { - const parsedBody = JSON.parse(body); + const parsedBody = JSON.parse(sanitizer.sanitize(body)); res.status(statusCode).json(parsedBody); } catch (e) { // Not a JSON body res.setHeader('content-type', 'text/plain'); - res.status(statusCode).send(body); + res.status(statusCode).send(sanitizer.sanitize(body)); } }; From bc13b18fd8da6a03f9e4aee0604693ad710438d9 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 10 May 2024 14:39:49 -0500 Subject: [PATCH 4/6] Update regex in cors origin to include root domain and subdomains of XFD and RSC. --- frontend/scripts/api.js | 4 ++-- frontend/scripts/docs.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/scripts/api.js b/frontend/scripts/api.js index f4c73a33..8b447f34 100644 --- a/frontend/scripts/api.js +++ b/frontend/scripts/api.js @@ -19,8 +19,8 @@ app.use((req, res, next) => { app.use( cors({ origin: [ - /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, - /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ + /^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/, + /^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/ ], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] }) diff --git a/frontend/scripts/docs.js b/frontend/scripts/docs.js index 02c60a0f..579be995 100644 --- a/frontend/scripts/docs.js +++ b/frontend/scripts/docs.js @@ -19,8 +19,8 @@ app.use(express.static(path.join(__dirname, '../docs/build'))); app.use( cors({ origin: [ - /^https:\/\/.*\.crossfeed\.cyber\.dhs\.gov$/, - /^https:\/\/.*\.readysetcyber\.cyber\.dhs\.gov$/ + /^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/, + /^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/ ], methods: 'GET' }) From 82170c1da8dc22c4b27a060855c338ed4e159b5b Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Fri, 17 May 2024 13:13:36 -0500 Subject: [PATCH 5/6] Parameterize CORS and Content Security Policy settings; add environment variable for policies to dev.env.example for local development. --- backend/env.yml | 8 ++++++++ backend/src/api/app.ts | 26 +++--------------------- dev.env.example | 5 +++++ frontend/scripts/api.js | 44 ++++++++++++---------------------------- frontend/scripts/docs.js | 18 ++++------------ 5 files changed, 33 insertions(+), 68 deletions(-) diff --git a/backend/env.yml b/backend/env.yml index f188883c..ba97bb44 100644 --- a/backend/env.yml +++ b/backend/env.yml @@ -9,6 +9,10 @@ staging: DB_NAME: ${ssm:/crossfeed/staging/DATABASE_NAME} DB_USERNAME: ${ssm:/crossfeed/staging/DATABASE_USER} DB_PASSWORD: ${ssm:/crossfeed/staging/DATABASE_PASSWORD} + CORS_MAIN: ${ssm:/crossfeed/staging/CORS_MAIN} + CORS_DOCS: ${ssm:/crossfeed/staging/CORS_DOCS} + CSP_MAIN: ${ssm:/crossfeed/staging/CSP_MAIN} + CSP_DOCS: ${ssm:/crossfeed/staging/CSP_DOCS} MDL_USERNAME: ${ssm:/crossfeed/staging/MDL_USERNAME} MDL_PASSWORD: ${ssm:/crossfeed/staging/MDL_PASSWORD} MDL_NAME: ${ssm:/crossfeed/staging/MDL_NAME} @@ -64,6 +68,10 @@ prod: DB_NAME: ${ssm:/crossfeed/prod/DATABASE_NAME} DB_USERNAME: ${ssm:/crossfeed/prod/DATABASE_USER} DB_PASSWORD: ${ssm:/crossfeed/prod/DATABASE_PASSWORD} + CORS_MAIN: ${ssm:/crossfeed/prod/CORS_MAIN} + CORS_DOCS: ${ssm:/crossfeed/prod/CORS_DOCS} + CSP_MAIN: ${ssm:/crossfeed/prod/CSP_MAIN} + CSP_DOCS: ${ssm:/crossfeed/prod/CSP_DOCS} MDL_USERNAME: ${ssm:/crossfeed/prod/MDL_USERNAME} MDL_PASSWORD: ${ssm:/crossfeed/prod/MDL_PASSWORD} MDL_NAME: ${ssm:/crossfeed/prod/MDL_NAME} diff --git a/backend/src/api/app.ts b/backend/src/api/app.ts index 57f3f16f..cc6739ca 100644 --- a/backend/src/api/app.ts +++ b/backend/src/api/app.ts @@ -70,32 +70,12 @@ app.use( app.use(express.json({ strict: false })); -app.use( - cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] - }) -); +const { origin, methods } = JSON.parse(process.env.CORS_MAIN!); +app.use(cors({ origin, methods })); app.use( helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: [ - "'self'", - 'https://cognito-idp.us-east-1.amazonaws.com', - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' - ], - objectSrc: ["'none'"], - scriptSrc: [ - "'self'", - 'https://api.staging-cd.crossfeed.cyber.dhs.gov' - // Add any other allowed script sources here - ], - frameAncestors: ["'none'"] - // Add other directives as needed - } - }, + contentSecurityPolicy: JSON.parse(process.env.CSP_MAIN!), hsts: { maxAge: 31536000, includeSubDomains: true, diff --git a/dev.env.example b/dev.env.example index 56dc76ad..83f05418 100644 --- a/dev.env.example +++ b/dev.env.example @@ -6,6 +6,11 @@ DB_PASSWORD=password DB_NAME=crossfeed JWT_SECRET=CHANGE_ME +CORS_MAIN={"origin":"http://localhost","methods":"GET,POST,PUT,DELETE,OPTIONS"} +CORS_DOCS={{"origin":"http://localhost","methods":"GET"}} +CSP_MAIN={"directives":{"defaultSrc":["'self'","http://localhost"],"frameSrc":["'self'","https://www.dhs.gov/ntas/"],"imgSrc":["'self'","http://localhost","https://www.dhs.gov"],"objectSrc":["'none'"],"scriptSrc":["'self'","http://localhost","https://www.dhs.gov"],"frameAncestors":["'none'"]}} +CSP_DOCS={"directives":{"baseUri":["'none'"],"defaultSrc":["'self'"],"frameAncestors":["'none'"],"objectSrc":["'none'"],"scriptSrc":["'none'"]}} + MDL_USERNAME=mdl MDL_PASSWORD=password MDL_NAME=crossfeed_mini_datalake diff --git a/frontend/scripts/api.js b/frontend/scripts/api.js index 8b447f34..e46b7b6e 100644 --- a/frontend/scripts/api.js +++ b/frontend/scripts/api.js @@ -16,43 +16,25 @@ app.use((req, res, next) => { next(); }); +app.use(express.json({ strict: false })); + +const { origin, methods } = JSON.parse(process.env.CORS_MAIN); +app.use(cors({ origin, methods })); + app.use( - cors({ - origin: [ - /^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/, - /^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/ - ], - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + helmet({ + contentSecurityPolicy: JSON.parse(process.env.CSP_MAIN), + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true + } }) ); app.use( helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: [ - "'self'", - 'https://cognito-idp.*.amazonaws.com', - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov' - ], - frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'], - imgSrc: [ - "'self'", - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov', - 'https://www.dhs.gov' - ], - objectSrc: ["'none'"], - scriptSrc: [ - "'self'", - 'https://*.crossfeed.cyber.dhs.gov', - 'https://*.readysetcyber.cyber.dhs.gov', - 'https://www.dhs.gov' - ], - frameAncestors: ["'none'"] - } - }, + contentSecurityPolicy: JSON.parse(process.env.CSP_MAIN), hsts: { maxAge: 31536000, includeSubDomains: true, diff --git a/frontend/scripts/docs.js b/frontend/scripts/docs.js index 579be995..a74b9da1 100644 --- a/frontend/scripts/docs.js +++ b/frontend/scripts/docs.js @@ -16,26 +16,16 @@ app.use( app.use(express.static(path.join(__dirname, '../docs/build'))); +const { origin, methods } = JSON.parse(process.env.CORS_DOCS); app.use( cors({ - origin: [ - /^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/, - /^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/ - ], - methods: 'GET' + origin, + methods }) ); app.use( helmet({ - contentSecurityPolicy: { - directives: { - baseUri: ["'none'"], - defaultSrc: ["'self'"], - frameAncestors: ["'none'"], - objectSrc: ["'none'"], - scriptSrc: ["'none'"] - } - }, + contentSecurityPolicy: JSON.parse(process.env.CSP_DOCS), hsts: { maxAge: 31536000, includeSubDomains: true, preload: true }, xFrameOptions: 'DENY' }) From 3131c736f9a046c2bc344138a89a7501dc35af01 Mon Sep 17 00:00:00 2001 From: "Grayson, Matthew" Date: Mon, 20 May 2024 08:49:35 -0500 Subject: [PATCH 6/6] Fix typo in dev.env.example for CORS_DOCS variable. --- dev.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.env.example b/dev.env.example index 83f05418..20c705b6 100644 --- a/dev.env.example +++ b/dev.env.example @@ -7,7 +7,7 @@ DB_NAME=crossfeed JWT_SECRET=CHANGE_ME CORS_MAIN={"origin":"http://localhost","methods":"GET,POST,PUT,DELETE,OPTIONS"} -CORS_DOCS={{"origin":"http://localhost","methods":"GET"}} +CORS_DOCS={"origin":"http://localhost","methods":"GET"} CSP_MAIN={"directives":{"defaultSrc":["'self'","http://localhost"],"frameSrc":["'self'","https://www.dhs.gov/ntas/"],"imgSrc":["'self'","http://localhost","https://www.dhs.gov"],"objectSrc":["'none'"],"scriptSrc":["'self'","http://localhost","https://www.dhs.gov"],"frameAncestors":["'none'"]}} CSP_DOCS={"directives":{"baseUri":["'none'"],"defaultSrc":["'self'"],"frameAncestors":["'none'"],"objectSrc":["'none'"],"scriptSrc":["'none'"]}}