Skip to content

Commit 2c009d0

Browse files
authored
fix(server): add CSP, cors (NangoHQ#2532)
## Describe your changes Fixes https://linear.app/nango/issue/NAN-1339/[pen-test]-fix-security-misconfiguration-exposed-in-pen-test Fixes https://linear.app/nango/issue/NAN-1340/[pen-test]-content-security-policy-csp-header-not-implemented Fixes https://linear.app/nango/issue/NAN-1454/remediate-medium-severity-vulnerabilities-from-pen-test - Add helmet.js to fix all notice related to SOC2 - CSP - No Sniff - No embed in iframe - HSTS - Setup stricter cors Allows all origin for public api and expose headers, restrict private api
1 parent 7181c1c commit 2c009d0

File tree

7 files changed

+139
-50
lines changed

7 files changed

+139
-50
lines changed

.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ WORKER_PORT=3004
3838
# - Configure server full URL (current value is the default for running Nango locally).
3939
#
4040
NANGO_SERVER_URL=http://localhost:3003
41+
CSP_REPORT_ONLY=false
4142
#
4243
#
4344
# - Configure server websockets path (current value is the default for running Nango locally).

docker-compose.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ services:
3131
- NANGO_DB_POOL_MAX=${NANGO_DB_POOL_MAX}
3232
- RECORDS_DATABASE_URL=${RECORDS_DATABASE_URL:-postgresql://nango:nango@nango-db:5432/nango}
3333
- SERVER_PORT=${SERVER_PORT}
34+
- CSP_REPORT_ONLY=true
3435
- NANGO_SERVER_URL=${NANGO_SERVER_URL:-http://localhost:3003}
3536
- NANGO_DASHBOARD_USERNAME=${NANGO_DASHBOARD_USERNAME}
3637
- NANGO_DASHBOARD_PASSWORD=${NANGO_DASHBOARD_PASSWORD}

package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { basePublicUrl, baseUrl } from '@nangohq/utils';
2+
import type { RequestHandler } from 'express';
3+
import helmet from 'helmet';
4+
5+
export function securityMiddlewares(): RequestHandler[] {
6+
const hostPublic = basePublicUrl;
7+
const hostApi = baseUrl;
8+
const reportOnly = process.env['CSP_REPORT_ONLY'];
9+
10+
return [
11+
helmet.xssFilter(),
12+
helmet.noSniff(),
13+
helmet.ieNoOpen(),
14+
helmet.frameguard({ action: 'sameorigin' }),
15+
helmet.dnsPrefetchControl(),
16+
helmet.hsts({
17+
maxAge: 5184000
18+
}),
19+
// == "Content-Security-Policy"
20+
helmet.contentSecurityPolicy({
21+
reportOnly: reportOnly !== 'false',
22+
directives: {
23+
defaultSrc: ["'self'", hostPublic, hostApi],
24+
childSrc: "'self'",
25+
connectSrc: ["'self'", 'https://*.google-analytics.com', 'https://*.sentry.io', hostPublic, hostApi, 'https://*.posthog.com'],
26+
fontSrc: ["'self'", 'https://*.googleapis.com', 'https://*.gstatic.com'],
27+
frameSrc: ["'self'", 'https://accounts.google.com'],
28+
imgSrc: ["'self'", 'data:', hostPublic, hostApi, 'https://*.google-analytics.com', 'https://*.googleapis.com', 'https://*.posthog.com'],
29+
manifestSrc: "'self'",
30+
mediaSrc: "'self'",
31+
objectSrc: "'self'",
32+
scriptSrc: [
33+
"'self'",
34+
"'unsafe-eval'",
35+
"'unsafe-inline'",
36+
hostPublic,
37+
hostApi,
38+
'https://*.google-analytics.com',
39+
'https://*.googleapis.com',
40+
'https://apis.google.com',
41+
'https://*.posthog.com'
42+
],
43+
styleSrc: ['blob:', "'self'", "'unsafe-inline'", 'https://*.googleapis.com', hostPublic, hostApi],
44+
workerSrc: ['blob:', "'self'", hostPublic, hostApi, 'https://*.googleapis.com', 'https://*.posthog.com']
45+
}
46+
})
47+
];
48+
}

packages/server/lib/routes.ts

+75-48
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import passport from 'passport';
2727
import environmentController from './controllers/environment.controller.js';
2828
import accountController from './controllers/account.controller.js';
2929
import type { Response, Request } from 'express';
30-
import { isCloud, isEnterprise, flagHasAuth, isBasicAuthEnabled, isTest, flagHasManagedAuth } from '@nangohq/utils';
30+
import { isCloud, isEnterprise, isBasicAuthEnabled, isTest, isLocal, basePublicUrl, baseUrl, flagHasAuth, flagHasManagedAuth } from '@nangohq/utils';
3131
import { errorManager } from '@nangohq/shared';
3232
import tracer from 'dd-trace';
3333
import { getConnection as getConnectionWeb } from './controllers/v1/connection/get.js';
@@ -66,12 +66,15 @@ import { patchUser } from './controllers/v1/user/patchUser.js';
6666
import { getInvite } from './controllers/v1/invite/getInvite.js';
6767
import { declineInvite } from './controllers/v1/invite/declineInvite.js';
6868
import { acceptInvite } from './controllers/v1/invite/acceptInvite.js';
69+
import { securityMiddlewares } from './middleware/security.js';
6970
import { getMeta } from './controllers/v1/meta/getMeta.js';
7071
import { postManagedSignup } from './controllers/v1/account/managed/postSignup.js';
7172
import { getManagedCallback } from './controllers/v1/account/managed/getCallback.js';
7273

7374
export const router = express.Router();
7475

76+
router.use(...securityMiddlewares());
77+
7578
const apiAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), rateLimiterMiddleware];
7679
const adminAuth = [authMiddleware.secretKeyAuth.bind(authMiddleware), authMiddleware.adminKeyAuth.bind(authMiddleware), rateLimiterMiddleware];
7780
const apiPublicAuth = [authMiddleware.publicKeyAuth.bind(authMiddleware), authCheck, rateLimiterMiddleware];
@@ -95,71 +98,95 @@ router.use(
9598
})
9699
);
97100
router.use(bodyParser.raw({ type: 'text/xml' }));
98-
router.use(cors());
99101
router.use(express.urlencoded({ extended: true }));
100102

101103
const upload = multer({ storage: multer.memoryStorage() });
102104

105+
// -------
103106
// API routes (no/public auth).
104107
router.get('/health', (_, res) => {
105108
res.status(200).send({ result: 'ok' });
106109
});
107110

108-
router.route('/oauth/callback').get(oauthController.oauthCallback.bind(oauthController));
109-
router.route('/webhook/:environmentUuid/:providerConfigKey').post(webhookController.receive.bind(proxyController));
110-
router.route('/app-auth/connect').get(appAuthController.connect.bind(appAuthController));
111-
router.route('/oauth/connect/:providerConfigKey').get(apiPublicAuth, oauthController.oauthRequest.bind(oauthController));
112-
router.route('/oauth2/auth/:providerConfigKey').post(apiPublicAuth, oauthController.oauth2RequestCC.bind(oauthController));
113-
router.route('/api-auth/api-key/:providerConfigKey').post(apiPublicAuth, apiAuthController.apiKey.bind(apiAuthController));
114-
router.route('/api-auth/basic/:providerConfigKey').post(apiPublicAuth, apiAuthController.basic.bind(apiAuthController));
115-
router.route('/app-store-auth/:providerConfigKey').post(apiPublicAuth, appStoreAuthController.auth.bind(appStoreAuthController));
116-
router.route('/auth/tba/:providerConfigKey').post(apiPublicAuth, tbaAuthorization);
117-
router.route('/unauth/:providerConfigKey').post(apiPublicAuth, unAuthController.create.bind(unAuthController));
111+
// -------
112+
// Public API routes
113+
const publicAPI = express.Router();
114+
const publicAPICorsHandler = cors({
115+
maxAge: 600,
116+
exposedHeaders: 'Authorization, Etag, Content-Type, Content-Length, X-Nango-Signature, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset',
117+
allowedHeaders: 'Nango-Activity-Log-Id, Nango-Is-Dry-Run, Nango-Is-Sync, Provider-Config-Key, Connection-Id',
118+
origin: '*'
119+
});
120+
publicAPI.use(publicAPICorsHandler);
121+
publicAPI.options('*', publicAPICorsHandler); // Pre-flight
122+
123+
publicAPI.route('/oauth/callback').get(oauthController.oauthCallback.bind(oauthController));
124+
publicAPI.route('/webhook/:environmentUuid/:providerConfigKey').post(webhookController.receive.bind(proxyController));
125+
publicAPI.route('/app-auth/connect').get(appAuthController.connect.bind(appAuthController));
126+
publicAPI.route('/oauth/connect/:providerConfigKey').get(apiPublicAuth, oauthController.oauthRequest.bind(oauthController));
127+
publicAPI.route('/oauth2/auth/:providerConfigKey').post(apiPublicAuth, oauthController.oauth2RequestCC.bind(oauthController));
128+
publicAPI.route('/api-auth/api-key/:providerConfigKey').post(apiPublicAuth, apiAuthController.apiKey.bind(apiAuthController));
129+
publicAPI.route('/api-auth/basic/:providerConfigKey').post(apiPublicAuth, apiAuthController.basic.bind(apiAuthController));
130+
publicAPI.route('/app-store-auth/:providerConfigKey').post(apiPublicAuth, appStoreAuthController.auth.bind(appStoreAuthController));
131+
publicAPI.route('/auth/tba/:providerConfigKey').post(apiPublicAuth, tbaAuthorization);
132+
publicAPI.route('/unauth/:providerConfigKey').post(apiPublicAuth, unAuthController.create.bind(unAuthController));
118133

119134
// API Admin routes
120-
router.route('/admin/flow/deploy/pre-built').post(adminAuth, flowController.adminDeployPrivateFlow.bind(flowController));
121-
router.route('/admin/customer').patch(adminAuth, accountController.editCustomer.bind(accountController));
135+
publicAPI.route('/admin/flow/deploy/pre-built').post(adminAuth, flowController.adminDeployPrivateFlow.bind(flowController));
136+
publicAPI.route('/admin/customer').patch(adminAuth, accountController.editCustomer.bind(accountController));
122137

123138
// API routes (API key auth).
124-
router.route('/provider').get(apiAuth, providerController.listProviders.bind(providerController));
125-
router.route('/provider/:provider').get(apiAuth, providerController.getProvider.bind(providerController));
126-
router.route('/config').get(apiAuth, configController.listProviderConfigs.bind(configController));
127-
router.route('/config/:providerConfigKey').get(apiAuth, configController.getProviderConfig.bind(configController));
128-
router.route('/config').post(apiAuth, configController.createProviderConfig.bind(configController));
129-
router.route('/config').put(apiAuth, configController.editProviderConfig.bind(configController));
130-
router.route('/config/:providerConfigKey').delete(apiAuth, configController.deleteProviderConfig.bind(configController));
131-
router.route('/connection/:connectionId').get(apiAuth, connectionController.getConnectionCreds.bind(connectionController));
132-
router.route('/connection').get(apiAuth, connectionController.listConnections.bind(connectionController));
133-
router.route('/connection/:connectionId').delete(apiAuth, connectionController.deleteConnection.bind(connectionController));
134-
router.route('/connection/:connectionId/metadata').post(apiAuth, connectionController.setMetadataLegacy.bind(connectionController));
135-
router.route('/connection/:connectionId/metadata').patch(apiAuth, connectionController.updateMetadataLegacy.bind(connectionController));
136-
router.route('/connection/metadata').post(apiAuth, setMetadata);
137-
router.route('/connection/metadata').patch(apiAuth, updateMetadata);
138-
router.route('/connection').post(apiAuth, connectionController.createConnection.bind(connectionController));
139-
router.route('/environment-variables').get(apiAuth, environmentController.getEnvironmentVariables.bind(connectionController));
140-
router.route('/sync/deploy').post(apiAuth, postDeploy);
141-
router.route('/sync/deploy/confirmation').post(apiAuth, postDeployConfirmation);
142-
router.route('/sync/update-connection-frequency').put(apiAuth, syncController.updateFrequencyForConnection.bind(syncController));
143-
router.route('/records').get(apiAuth, syncController.getAllRecords.bind(syncController));
144-
router.route('/sync/trigger').post(apiAuth, syncController.trigger.bind(syncController));
145-
router.route('/sync/pause').post(apiAuth, syncController.pause.bind(syncController));
146-
router.route('/sync/start').post(apiAuth, syncController.start.bind(syncController));
147-
router.route('/sync/provider').get(apiAuth, syncController.getSyncProvider.bind(syncController));
148-
router.route('/sync/status').get(apiAuth, syncController.getSyncStatus.bind(syncController));
149-
router.route('/sync/:syncId').delete(apiAuth, syncController.deleteSync.bind(syncController));
150-
router.route('/flow/attributes').get(apiAuth, syncController.getFlowAttributes.bind(syncController));
151-
router.route('/flow/configs').get(apiAuth, flowController.getFlowConfig.bind(flowController));
152-
router.route('/scripts/config').get(apiAuth, flowController.getFlowConfig.bind(flowController));
153-
router.route('/action/trigger').post(apiAuth, syncController.triggerAction.bind(syncController)); //TODO: to deprecate
154-
155-
router.route('/v1/*').all(apiAuth, syncController.actionOrModel.bind(syncController));
156-
157-
router.route('/proxy/*').all(apiAuth, upload.any(), proxyController.routeCall.bind(proxyController));
139+
publicAPI.route('/provider').get(apiAuth, providerController.listProviders.bind(providerController));
140+
publicAPI.route('/provider/:provider').get(apiAuth, providerController.getProvider.bind(providerController));
141+
publicAPI.route('/config').get(apiAuth, configController.listProviderConfigs.bind(configController));
142+
publicAPI.route('/config/:providerConfigKey').get(apiAuth, configController.getProviderConfig.bind(configController));
143+
publicAPI.route('/config').post(apiAuth, configController.createProviderConfig.bind(configController));
144+
publicAPI.route('/config').put(apiAuth, configController.editProviderConfig.bind(configController));
145+
publicAPI.route('/config/:providerConfigKey').delete(apiAuth, configController.deleteProviderConfig.bind(configController));
146+
publicAPI.route('/connection/:connectionId').get(apiAuth, connectionController.getConnectionCreds.bind(connectionController));
147+
publicAPI.route('/connection').get(apiAuth, connectionController.listConnections.bind(connectionController));
148+
publicAPI.route('/connection/:connectionId').delete(apiAuth, connectionController.deleteConnection.bind(connectionController));
149+
publicAPI.route('/connection/:connectionId/metadata').post(apiAuth, connectionController.setMetadataLegacy.bind(connectionController));
150+
publicAPI.route('/connection/:connectionId/metadata').patch(apiAuth, connectionController.updateMetadataLegacy.bind(connectionController));
151+
publicAPI.route('/connection/metadata').post(apiAuth, setMetadata);
152+
publicAPI.route('/connection/metadata').patch(apiAuth, updateMetadata);
153+
publicAPI.route('/connection').post(apiAuth, connectionController.createConnection.bind(connectionController));
154+
publicAPI.route('/environment-variables').get(apiAuth, environmentController.getEnvironmentVariables.bind(connectionController));
155+
publicAPI.route('/sync/deploy').post(apiAuth, postDeploy);
156+
publicAPI.route('/sync/deploy/confirmation').post(apiAuth, postDeployConfirmation);
157+
publicAPI.route('/sync/update-connection-frequency').put(apiAuth, syncController.updateFrequencyForConnection.bind(syncController));
158+
publicAPI.route('/records').get(apiAuth, syncController.getAllRecords.bind(syncController));
159+
publicAPI.route('/sync/trigger').post(apiAuth, syncController.trigger.bind(syncController));
160+
publicAPI.route('/sync/pause').post(apiAuth, syncController.pause.bind(syncController));
161+
publicAPI.route('/sync/start').post(apiAuth, syncController.start.bind(syncController));
162+
publicAPI.route('/sync/provider').get(apiAuth, syncController.getSyncProvider.bind(syncController));
163+
publicAPI.route('/sync/status').get(apiAuth, syncController.getSyncStatus.bind(syncController));
164+
publicAPI.route('/sync/:syncId').delete(apiAuth, syncController.deleteSync.bind(syncController));
165+
publicAPI.route('/flow/attributes').get(apiAuth, syncController.getFlowAttributes.bind(syncController));
166+
publicAPI.route('/flow/configs').get(apiAuth, flowController.getFlowConfig.bind(flowController));
167+
publicAPI.route('/scripts/config').get(apiAuth, flowController.getFlowConfig.bind(flowController));
168+
publicAPI.route('/action/trigger').post(apiAuth, syncController.triggerAction.bind(syncController)); //TODO: to deprecate
169+
170+
publicAPI.route('/v1/*').all(apiAuth, syncController.actionOrModel.bind(syncController));
158171

172+
publicAPI.route('/proxy/*').all(apiAuth, upload.any(), proxyController.routeCall.bind(proxyController));
173+
174+
router.use(publicAPI);
175+
176+
// -------
159177
// Webapp routes (session auth).
160178
const web = express.Router();
161179
setupAuth(web);
162180

181+
const webCorsHandler = cors({
182+
maxAge: 600,
183+
exposedHeaders: 'Authorization, Etag, Content-Type, Content-Length, Set-Cookie',
184+
origin: isLocal ? '*' : [basePublicUrl, baseUrl],
185+
credentials: true
186+
});
187+
web.use(webCorsHandler);
188+
web.options('*', webCorsHandler); // Pre-flight
189+
163190
// Webapp routes (no auth).
164191
if (flagHasAuth) {
165192
web.route('/api/v1/account/signup').post(rateLimiterMiddleware, signup);

packages/server/lib/server.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const { NANGO_MIGRATE_AT_START = 'true' } = process.env;
2020
const logger = getLogger('Server');
2121

2222
const app = express();
23+
app.disable('x-powered-by');
24+
app.set('trust proxy', 1);
2325

2426
// Log all requests
2527
if (process.env['ENABLE_REQUEST_LOG'] !== 'false') {

packages/server/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"@nangohq/records": "file:../records",
3131
"@nangohq/shared": "file:../shared",
3232
"@nangohq/types": "file:../types",
33-
"@nangohq/webhooks": "file:../webhooks",
3433
"@nangohq/utils": "file:../utils",
34+
"@nangohq/webhooks": "file:../webhooks",
3535
"@workos-inc/node": "^6.2.0",
3636
"axios": "^1.3.4",
3737
"body-parser": "1.20.2",
@@ -44,6 +44,7 @@
4444
"express": "^4.19.2",
4545
"express-session": "^1.17.3",
4646
"form-data": "^4.0.0",
47+
"helmet": "7.1.0",
4748
"jsonwebtoken": "^9.0.2",
4849
"lodash-es": "^4.17.21",
4950
"mailgun.js": "^8.2.1",
@@ -79,8 +80,8 @@
7980
"@types/ws": "^8.5.4",
8081
"get-port": "7.1.0",
8182
"nodemon": "^3.0.1",
82-
"typescript": "5.3.3",
8383
"type-fest": "4.14.0",
84+
"typescript": "5.3.3",
8485
"vitest": "1.6.0"
8586
}
8687
}

0 commit comments

Comments
 (0)