Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .env.development.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# --- Auth/Client Config ---
CLIENT_ID=
APP_API_BASE_URL=
AUTH_API_BASE_URL=
ENABLE_SESSION_TIMEOUT=
STORAGE_SESSION_TIME_KEY=
EXPIRY_TIME_IN_MINUTE=
PROMPT_TIME_BEFORE_IDLE_IN_MINUTE=
# --- CSP Directives ---
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

VITE_CSP_DEFAULT_SRC='self'
VITE_CSP_SCRIPT_SRC='self'
VITE_CSP_STYLE_SRC='self'
VITE_CSP_IMG_SRC='self data:'
VITE_CSP_CONNECT_SRC='self env:APP_API_BASE_URL env:AUTH_API_BASE_URL'
VITE_CSP_OBJECT_SRC='none'
VITE_CSP_BASE_URI='self'
VITE_CSP_FORM_ACTION='self'
VITE_CSP_BLOCK_ALL_MIXED_CONTENT=true
# VITE_CSP_REPORT_URI= # Optional: Set to your CSP report endpoint
# VITE_CSP_REPORT_TO= # Optional: Set to your CSP report-to endpoint
# For nonce/hash support, add e.g. 'nonce-{NONCE}' or 'sha256-...' to the relevant directive
48 changes: 47 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
# --- Auth/Client Config ---
CLIENT_ID=
APP_API_BASE_URL=
AUTH_API_BASE_URL=
ENABLE_SESSION_TIMEOUT=
STORAGE_SESSION_TIME_KEY=
EXPIRY_TIME_IN_MINUTE=
PROMPT_TIME_BEFORE_IDLE_IN_MINUTE=
PROMPT_TIME_BEFORE_IDLE_IN_MINUTE=

# Content Security Policy Directives
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

# Default policy: only allow resources from this origin
VITE_CSP_DEFAULT_SRC='self'

# Only allow scripts from this origin (add nonce/hash as needed)
VITE_CSP_SCRIPT_SRC='self'
# Example for nonce/hash support (future): 'self' 'nonce-<NONCE>' 'sha256-<HASH>'

# Only allow styles from this origin
VITE_CSP_STYLE_SRC='self'

# Allow images from this origin and data URIs
VITE_CSP_IMG_SRC='self data:'

# Only allow connections to this origin (API, websockets, etc.)
# --- Dynamic variable expansion example ---
# You can reference other env vars in CSP directives using env:VITE_SOME_KEY
# Example: Allow API and Auth endpoints in connect-src
VITE_CSP_CONNECT_SRC='self env:APP_API_BASE_URL env:AUTH_API_BASE_URL'
# This will expand to: connect-src 'self' <APP_API_BASE_URL> <AUTH_API_BASE_URL>;
# Disallow all plugins/objects
VITE_CSP_OBJECT_SRC='none'

# Only allow base URIs from this origin
VITE_CSP_BASE_URI='self'

# Only allow form submissions to this origin
VITE_CSP_FORM_ACTION='self'

# Block all mixed content (HTTP on HTTPS)
VITE_CSP_BLOCK_ALL_MIXED_CONTENT=true

# Optional: Set to your CSP report endpoint (for violation reporting)
# VITE_CSP_REPORT_URI=https://your-report-endpoint.example.com/csp-violation

# Optional: Set to your CSP report-to endpoint (for violation reporting)
# VITE_CSP_REPORT_TO=your-report-group

# For nonce/hash support, add e.g. 'nonce-{NONCE}' or 'sha256-...' to the relevant directive above

# --- Any other required keys ---
# ...
23 changes: 23 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# --- Auth/Client Config ---
CLIENT_ID=
APP_API_BASE_URL=
AUTH_API_BASE_URL=
ENABLE_SESSION_TIMEOUT=
STORAGE_SESSION_TIME_KEY=
EXPIRY_TIME_IN_MINUTE=
PROMPT_TIME_BEFORE_IDLE_IN_MINUTE=
# --- CSP Directives ---
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

VITE_CSP_DEFAULT_SRC='self'
VITE_CSP_SCRIPT_SRC='self'
VITE_CSP_STYLE_SRC='self'
VITE_CSP_IMG_SRC='self data:'
VITE_CSP_CONNECT_SRC='self env:APP_API_BASE_URL env:AUTH_API_BASE_URL'
VITE_CSP_OBJECT_SRC='none'
VITE_CSP_BASE_URI='self'
VITE_CSP_FORM_ACTION='self'
VITE_CSP_BLOCK_ALL_MIXED_CONTENT=true
# VITE_CSP_REPORT_URI= # Optional: Set to your CSP report endpoint
# VITE_CSP_REPORT_TO= # Optional: Set to your CSP report-to endpoint
# For nonce/hash support, add e.g. 'nonce-{NONCE}' or 'sha256-...' to the relevant directive
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
.env.test.local
.env.production.local
.env.production
.env.development

npm-debug.log*
yarn-debug.log*
Expand Down
64 changes: 56 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-beautiful-dnd": "^13.1.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^2.0.5",
"react-idle-timer": "^5.7.2",
"react-redux": "^8.1.0",
"react-router": "^6.4.3",
Expand Down
101 changes: 101 additions & 0 deletions src/Components/CSP.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Helmet } from 'react-helmet-async';
/**
* CSPMeta generates a Content Security Policy (CSP) meta tag using environment variables (VITE_CSP_*).
* - You can reference other env vars in CSP directives using env:VITE_SOME_KEY (e.g., env:APP_API_BASE_URL or env:AUTH_API_BASE_URL).
* Example: VITE_CSP_CONNECT_SRC='self env:APP_API_BASE_URL env:AUTH_API_BASE_URL' will expand to connect-src 'self' <APP_API_BASE_URL> <AUTH_API_BASE_URL>;
* - Validates and warns for missing critical directives.
* - Provides secure defaults for critical directives if missing.
* - Supports reporting directives.
* - Documents where nonce/hash support can be added in the future.
*/

const DEFAULT_CSP = {
'default-src': "'self'",
'script-src': "'self'",
'style-src': "'self'",
'img-src': "'self' data:",
'connect-src': "'self'",
'object-src': "'none'",
'base-uri': "'self'",
'form-action': "'self'",
'block-all-mixed-content': true,
};

const CRITICAL_DIRECTIVES = ['default-src', 'script-src', 'object-src'];

function getCspDirectives(env: Record<string, string | boolean>) {
const directives: string[] = [];
let hasCritical = { 'default-src': false, 'script-src': false, 'object-src': false };

Object.entries(env).forEach(([key, rawValue]) => {
if (!key.startsWith('VITE_CSP_')) return;

const directiveKey = key
.replace(/^VITE_CSP_/, '')
.replace(/_/g, '-')
.toLowerCase();

let value = typeof rawValue === 'string' ? rawValue.trim() : rawValue;
if (!value) return;

if (value === 'true' || value === true) {
directives.push(`${directiveKey};`);
} else {
const parts = (value as string)
.split(' ')
.map((p: string) => {
if (p.startsWith('env:')) {
const envVarKey = p.slice(4);
return env[envVarKey] ?? null;
}
// --- FUTURE: Add nonce/hash support here ---
// If p === 'nonce-{SOME_NONCE}', replace with actual nonce value
// If p === 'sha256-...', allow hash values
return p;
})
.filter(Boolean);
if (parts.length > 0) {
directives.push(`${directiveKey} ${parts.join(' ')};`);
}
}
if (CRITICAL_DIRECTIVES.includes(directiveKey)) {
hasCritical[directiveKey] = true;
}
});

// Add defaults for missing critical directives
CRITICAL_DIRECTIVES.forEach((dir) => {
if (!hasCritical[dir]) {
// eslint-disable-next-line no-console
console.warn(`[CSP] Missing critical directive "${dir}", using secure default: ${DEFAULT_CSP[dir]}`);
if (DEFAULT_CSP[dir] === true) {
directives.push(`${dir};`);
} else {
directives.push(`${dir} ${DEFAULT_CSP[dir]};`);
}
}
});

// Add reporting directives if present
if (env.VITE_CSP_REPORT_URI) {
directives.push(`report-uri ${env.VITE_CSP_REPORT_URI};`);
}
if (env.VITE_CSP_REPORT_TO) {
directives.push(`report-to ${env.VITE_CSP_REPORT_TO};`);
}

return directives.join(' ').trim();
}

const CSPMeta = () => {
const env = import.meta.env;
const csp = getCspDirectives(env);

return (
<Helmet>
<meta httpEquiv="Content-Security-Policy" content={csp} />
</Helmet>
);
};

export default CSPMeta;
8 changes: 6 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import {HelmetProvider} from 'react-helmet-async';
import AppWrapper from './AppWrapper';

import CSPMeta from './Components/CSP';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
<React.StrictMode>
<AppWrapper />
<HelmetProvider>
<CSPMeta />
<AppWrapper />
</HelmetProvider>
</React.StrictMode>,
);
Loading