changes added wih for VAPT compatibility
This commit is contained in:
parent
bdfda74167
commit
a06a1bfeec
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
import{a as s}from"./index-x1JLuWho.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DbB0YGPu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B1UBYWWO.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-DX2Gwh6C.js.map
|
||||
import{a as s}from"./index-BFWo0Bu2.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DyksGUTu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B_rK4TXr.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-Cixl24m2.js.map
|
||||
@ -1 +1 @@
|
||||
{"version":3,"file":"conclusionApi-DX2Gwh6C.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
||||
{"version":3,"file":"conclusionApi-Cixl24m2.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
||||
File diff suppressed because one or more lines are too long
1
build/assets/index-BFWo0Bu2.js.map
Normal file
1
build/assets/index-BFWo0Bu2.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/assets/index-D5NCgjQR.css
Normal file
1
build/assets/index-D5NCgjQR.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
build/assets/ui-vendor-DyksGUTu.js
Normal file
3
build/assets/ui-vendor-DyksGUTu.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,69 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- CSP: Allows blob URLs for file previews and cross-origin API calls during development -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="description"
|
||||
content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="theme-color" content="#2d4a3e" />
|
||||
<title>Royal Enfield | Approval Portal</title>
|
||||
|
||||
<!-- Preload critical fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<!-- Ensure proper icon rendering and layout -->
|
||||
<style>
|
||||
/* Ensure Lucide icons render properly */
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Fix for icon alignment in buttons */
|
||||
button svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Ensure proper text rendering */
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* Fix for mobile viewport and sidebar */
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure proper sidebar toggle behavior */
|
||||
.sidebar-toggle {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Fix for icon button hover states */
|
||||
button:hover svg {
|
||||
transform: scale(1.05);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-x1JLuWho.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
|
||||
<script type="module" crossorigin src="/assets/index-BFWo0Bu2.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-DbB0YGPu.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-DyksGUTu.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B1UBYWWO.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CNlPctO6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B_rK4TXr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D5NCgjQR.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@ -42,6 +42,7 @@
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.17.0",
|
||||
"winston-loki": "^6.1.3",
|
||||
"xss": "^1.0.15",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -5488,6 +5489,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssfilter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
|
||||
"integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
@ -12120,6 +12127,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xss": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz",
|
||||
"integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^2.20.3",
|
||||
"cssfilter": "0.0.10"
|
||||
},
|
||||
"bin": {
|
||||
"xss": "bin/xss"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xss/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.17.0",
|
||||
"winston-loki": "^6.1.3",
|
||||
"xss": "^1.0.15",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
74
scripts/verify_security.js
Normal file
74
scripts/verify_security.js
Normal file
@ -0,0 +1,74 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
async function verifySecurity() {
|
||||
try {
|
||||
console.log('--- Verifying Security Fixes ---');
|
||||
|
||||
console.log('\n1. Verifying Security Headers...');
|
||||
const response = await axios.get(`${BASE_URL}/health`);
|
||||
const headers = response.headers;
|
||||
|
||||
console.log('\n1b. Verifying Security Headers on 404...');
|
||||
try {
|
||||
const res404 = await axios.get(`${BASE_URL}/non-existent`, { validateStatus: false });
|
||||
console.log('404 Status:', res404.status);
|
||||
console.log('404 CSP:', res404.headers['content-security-policy']);
|
||||
|
||||
console.log('\n1c. Verifying Security Headers on /assets (Redirect check)...');
|
||||
const resAssets = await axios.get(`${BASE_URL}/assets`, {
|
||||
validateStatus: false,
|
||||
maxRedirects: 0 // Don't follow to see the first response (likely 301)
|
||||
});
|
||||
console.log('Assets Status:', resAssets.status);
|
||||
console.log('Assets CSP:', resAssets.headers['content-security-policy']);
|
||||
} catch (e) {
|
||||
console.log('Error checking 404/Redirect:', e.message);
|
||||
if (e.response) {
|
||||
console.log('Response Status:', e.response.status);
|
||||
console.log('Response CSP:', e.response.headers['content-security-policy']);
|
||||
}
|
||||
}
|
||||
|
||||
// Check CSP
|
||||
const csp = headers['content-security-policy'];
|
||||
console.log('CSP:', csp);
|
||||
if (csp && csp.includes("frame-ancestors 'self'")) {
|
||||
console.log('✅ Clickjacking Protection (frame-ancestors) is present.');
|
||||
} else {
|
||||
console.log('❌ Clickjacking Protection (frame-ancestors) is MISSING.');
|
||||
}
|
||||
|
||||
// Check X-Frame-Options
|
||||
const xfo = headers['x-frame-options'];
|
||||
console.log('X-Frame-Options:', xfo);
|
||||
if (xfo === 'SAMEORIGIN') {
|
||||
console.log('✅ X-Frame-Options: SAMEORIGIN is present.');
|
||||
} else {
|
||||
console.log('❌ X-Frame-Options: SAMEORIGIN is MISSING.');
|
||||
}
|
||||
|
||||
console.log('\n2. Verifying Cookie Security Flags (requires login)...');
|
||||
console.log('Note: This is best verified in a real browser or by checking the code changes in auth.controller.ts.');
|
||||
|
||||
console.log('\n3. Verifying Sanitization Utility...');
|
||||
// This is verified by the unit test if we create one, but we can also do a manual check if the server is running.
|
||||
|
||||
console.log('\n--- Verification Summary ---');
|
||||
console.log('Content-Security-Policy: frame-ancestors added.');
|
||||
console.log('X-Frame-Options: set to SAMEORIGIN.');
|
||||
console.log('Cookie flags: sameSite set to lax, secure flag ensured in production.');
|
||||
console.log('Sanitization: Implemented in WorkNotes, Holidays, Workflow Requests, and Conclusions.');
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
console.error('❌ Error: Could not connect to the backend server at', BASE_URL);
|
||||
console.error('Please ensure the server is running (npm run dev).');
|
||||
} else {
|
||||
console.error('❌ Error during verification:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifySecurity();
|
||||
114
src/app.ts
114
src/app.ts
@ -19,6 +19,65 @@ dotenv.config();
|
||||
// Secrets are now initialized in server.ts before app is imported
|
||||
|
||||
const app: express.Application = express();
|
||||
|
||||
// 1. Security middleware - Manual "Gold Standard" CSP to ensure it survives 301/404/etc.
|
||||
// This handles a specific Express/Helmet edge case where redirects lose headers.
|
||||
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
|
||||
// Build connect-src dynamically
|
||||
const connectSrc = ["'self'", "blob:", "data:"];
|
||||
if (isDev) {
|
||||
connectSrc.push("http://localhost:3000", "http://localhost:5000", "ws://localhost:3000", "ws://localhost:5000");
|
||||
if (frontendUrl.includes('localhost')) connectSrc.push(frontendUrl);
|
||||
} else if (frontendUrl && frontendUrl !== '*') {
|
||||
const origins = frontendUrl.split(',').map(url => url.trim()).filter(Boolean);
|
||||
connectSrc.push(...origins);
|
||||
}
|
||||
|
||||
// Define strict CSP directives
|
||||
const directives = [
|
||||
"default-src 'none'",
|
||||
// sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= is for empty <style> tags injected by libraries
|
||||
"style-src 'self' https://fonts.googleapis.com 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='",
|
||||
"style-src-elem 'self' https://fonts.googleapis.com 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='",
|
||||
// style-src-attr 'unsafe-inline' is required for React's dynamic style prop (Progress bars, Framer Motion, layout logic)
|
||||
"style-src-attr 'unsafe-inline'",
|
||||
"script-src 'self'",
|
||||
"script-src-elem 'self'",
|
||||
"script-src-attr 'none'",
|
||||
"img-src 'self' data: blob: https://*.royalenfield.com https://*.okta.com https://*.oktapreview.com https://*.googleapis.com https://*.gstatic.com",
|
||||
`connect-src ${connectSrc.join(' ')}`,
|
||||
"frame-src 'self' blob: data:",
|
||||
"font-src 'self' https://fonts.gstatic.com data:",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'self'",
|
||||
"worker-src 'self' blob:",
|
||||
"manifest-src 'self'",
|
||||
!isDev ? "upgrade-insecure-requests" : ""
|
||||
].filter(Boolean).join("; ");
|
||||
|
||||
res.setHeader('Content-Security-Policy', directives);
|
||||
next();
|
||||
});
|
||||
|
||||
// Configure other security headers via Helmet (with CSP disabled since we set it manually)
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false, // Handled manually above to ensure redirect compatibility
|
||||
crossOriginEmbedderPolicy: false,
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" },
|
||||
xFrameOptions: { action: "sameorigin" },
|
||||
}));
|
||||
|
||||
// 2. CORS middleware - MUST be before other middleware
|
||||
app.use(corsMiddleware);
|
||||
|
||||
// 3. Cookie parser middleware - MUST be before routes
|
||||
app.use(cookieParser());
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
// Initialize database connection
|
||||
@ -43,61 +102,6 @@ if (process.env.TRUST_PROXY === 'true' || process.env.NODE_ENV === 'production')
|
||||
app.set('trust proxy', 1);
|
||||
}
|
||||
|
||||
// CORS middleware - MUST be before other middleware
|
||||
app.use(corsMiddleware);
|
||||
|
||||
// Security middleware - Configure Helmet to work with CORS
|
||||
// Get frontend URL for CSP - allow cross-origin connections in development
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Build connect-src directive - allow backend API and blob URLs
|
||||
const connectSrc = ["'self'", "blob:", "data:"];
|
||||
if (isDevelopment) {
|
||||
// In development, allow connections to common dev ports
|
||||
connectSrc.push("http://localhost:3000", "http://localhost:5000", "ws://localhost:3000", "ws://localhost:5000");
|
||||
// Also allow the configured frontend URL if it's a localhost URL
|
||||
if (frontendUrl.includes('localhost')) {
|
||||
connectSrc.push(frontendUrl);
|
||||
}
|
||||
} else {
|
||||
// In production, only allow the configured frontend URL
|
||||
if (frontendUrl && frontendUrl !== '*') {
|
||||
const frontendOrigins = frontendUrl.split(',').map(url => url.trim()).filter(Boolean);
|
||||
connectSrc.push(...frontendOrigins);
|
||||
}
|
||||
}
|
||||
|
||||
// Build CSP directives - conditionally include upgradeInsecureRequests
|
||||
const cspDirectives: any = {
|
||||
defaultSrc: ["'self'", "blob:"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
connectSrc: connectSrc,
|
||||
frameSrc: ["'self'", "blob:"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'self'"],
|
||||
formAction: ["'self'"],
|
||||
};
|
||||
|
||||
// Only add upgradeInsecureRequests in production (it forces HTTPS)
|
||||
if (!isDevelopment) {
|
||||
cspDirectives.upgradeInsecureRequests = [];
|
||||
}
|
||||
|
||||
app.use(helmet({
|
||||
crossOriginEmbedderPolicy: false,
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" },
|
||||
contentSecurityPolicy: {
|
||||
directives: cspDirectives,
|
||||
},
|
||||
}));
|
||||
|
||||
// Cookie parser middleware - MUST be before routes
|
||||
app.use(cookieParser());
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
@ -8,6 +8,7 @@ import logger from '@utils/logger';
|
||||
import { initializeHolidaysCache, clearWorkingHoursCache } from '@utils/tatTimeUtils';
|
||||
import { clearConfigCache } from '@services/configReader.service';
|
||||
import { User, UserRole } from '@models/User';
|
||||
import { sanitizeHtml } from '@utils/sanitizer';
|
||||
|
||||
/**
|
||||
* Get all holidays (with optional year filter)
|
||||
@ -103,7 +104,7 @@ export const createHoliday = async (req: Request, res: Response): Promise<void>
|
||||
const holiday = await holidayService.createHoliday({
|
||||
holidayDate,
|
||||
holidayName,
|
||||
description,
|
||||
description: description ? sanitizeHtml(description) : description,
|
||||
holidayType: holidayType || HolidayType.ORGANIZATIONAL,
|
||||
isRecurring: isRecurring || false,
|
||||
recurrenceRule,
|
||||
@ -145,6 +146,9 @@ export const updateHoliday = async (req: Request, res: Response): Promise<void>
|
||||
|
||||
const { holidayId } = req.params;
|
||||
const updates = req.body;
|
||||
if (updates.description) {
|
||||
updates.description = sanitizeHtml(updates.description);
|
||||
}
|
||||
|
||||
const holiday = await holidayService.updateHoliday(holidayId, updates, userId);
|
||||
|
||||
@ -389,7 +393,7 @@ export const updateConfiguration = async (req: Request, res: Response): Promise<
|
||||
}
|
||||
|
||||
const { configKey } = req.params;
|
||||
const { configValue } = req.body;
|
||||
let { configValue } = req.body;
|
||||
|
||||
if (configValue === undefined) {
|
||||
res.status(400).json({
|
||||
@ -399,6 +403,12 @@ export const updateConfiguration = async (req: Request, res: Response): Promise<
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize config value if it's likely to be rendered as HTML
|
||||
// We can be selective or just sanitize all strings for safety
|
||||
if (typeof configValue === 'string') {
|
||||
configValue = sanitizeHtml(configValue);
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
const result = await sequelize.query(`
|
||||
UPDATE admin_configurations
|
||||
|
||||
@ -135,7 +135,7 @@ export class AuthController {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? 'none' as const : 'lax' as const, // 'none' for cross-domain in production
|
||||
sameSite: isProduction ? 'lax' as const : 'lax' as const, // 'lax' is safer and works on same-domain
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
};
|
||||
|
||||
@ -209,7 +209,7 @@ export class AuthController {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? ('none' as const) : ('lax' as const),
|
||||
sameSite: isProduction ? ('lax' as const) : ('lax' as const),
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
path: '/',
|
||||
};
|
||||
@ -259,7 +259,7 @@ export class AuthController {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? ('none' as const) : ('lax' as const),
|
||||
sameSite: isProduction ? ('lax' as const) : ('lax' as const),
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
path: '/',
|
||||
};
|
||||
@ -472,7 +472,7 @@ export class AuthController {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? 'none' as const : 'lax' as const,
|
||||
sameSite: isProduction ? 'lax' as const : 'lax' as const,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
};
|
||||
|
||||
@ -552,7 +552,7 @@ export class AuthController {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: isProduction ? 'none' as const : 'lax' as const, // 'none' for cross-domain in production
|
||||
sameSite: isProduction ? 'lax' as const : 'lax' as const, // 'lax' for same-domain
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours for access token
|
||||
};
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { aiService } from '@services/ai.service';
|
||||
import { activityService } from '@services/activity.service';
|
||||
import logger from '@utils/logger';
|
||||
import { getRequestMetadata } from '@utils/requestUtils';
|
||||
import { sanitizeHtml } from '@utils/sanitizer';
|
||||
|
||||
export class ConclusionController {
|
||||
/**
|
||||
@ -249,9 +250,10 @@ export class ConclusionController {
|
||||
|
||||
// Update conclusion
|
||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||
const sanitizedRemark = sanitizeHtml(finalRemark);
|
||||
|
||||
await conclusion.update({
|
||||
finalRemark: finalRemark,
|
||||
finalRemark: sanitizedRemark,
|
||||
editedBy: userId,
|
||||
isEdited: wasEdited,
|
||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount
|
||||
@ -283,6 +285,8 @@ export class ConclusionController {
|
||||
return res.status(400).json({ error: 'Final remark is required' });
|
||||
}
|
||||
|
||||
const sanitizedRemark = sanitizeHtml(finalRemark);
|
||||
|
||||
// Fetch request
|
||||
const request = await WorkflowRequest.findOne({
|
||||
where: { requestId },
|
||||
@ -315,7 +319,7 @@ export class ConclusionController {
|
||||
aiGeneratedRemark: null,
|
||||
aiModelUsed: null,
|
||||
aiConfidenceScore: null,
|
||||
finalRemark: finalRemark,
|
||||
finalRemark: sanitizedRemark,
|
||||
editedBy: userId,
|
||||
isEdited: false,
|
||||
editCount: 0,
|
||||
@ -330,7 +334,7 @@ export class ConclusionController {
|
||||
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
|
||||
|
||||
await conclusion.update({
|
||||
finalRemark: finalRemark,
|
||||
finalRemark: sanitizedRemark,
|
||||
editedBy: userId,
|
||||
isEdited: wasEdited,
|
||||
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount,
|
||||
@ -341,7 +345,7 @@ export class ConclusionController {
|
||||
// Update request status to CLOSED
|
||||
await request.update({
|
||||
status: 'CLOSED',
|
||||
conclusionRemark: finalRemark,
|
||||
conclusionRemark: sanitizedRemark,
|
||||
closureDate: new Date()
|
||||
} as any);
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import { notificationService } from './notification.service';
|
||||
import { activityService } from './activity.service';
|
||||
import { tatSchedulerService } from './tatScheduler.service';
|
||||
import { emitToRequestRoom } from '../realtime/socket';
|
||||
import { sanitizeHtml } from '@utils/sanitizer';
|
||||
|
||||
export class WorkflowService {
|
||||
/**
|
||||
@ -2458,8 +2459,8 @@ export class WorkflowService {
|
||||
requestNumber,
|
||||
initiatorId,
|
||||
templateType: workflowData.templateType,
|
||||
title: workflowData.title,
|
||||
description: workflowData.description,
|
||||
title: workflowData.title ? sanitizeHtml(workflowData.title) : workflowData.title,
|
||||
description: workflowData.description ? sanitizeHtml(workflowData.description) : workflowData.description,
|
||||
priority: workflowData.priority,
|
||||
currentLevel: 1,
|
||||
totalLevels: workflowData.approvalLevels.length,
|
||||
@ -3109,6 +3110,14 @@ export class WorkflowService {
|
||||
|
||||
async updateWorkflow(requestId: string, updateData: UpdateWorkflowRequest): Promise<WorkflowRequest | null> {
|
||||
try {
|
||||
// Sanitize title and description if provided
|
||||
if (updateData.title) {
|
||||
updateData.title = sanitizeHtml(updateData.title);
|
||||
}
|
||||
if (updateData.description) {
|
||||
updateData.description = sanitizeHtml(updateData.description);
|
||||
}
|
||||
|
||||
const workflow = await this.findWorkflowByIdentifier(requestId);
|
||||
if (!workflow) return null;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { gcsStorageService } from './gcsStorage.service';
|
||||
import logger from '@utils/logger';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { sanitizeHtml } from '@utils/sanitizer';
|
||||
|
||||
export class WorkNoteService {
|
||||
async list(requestId: string) {
|
||||
@ -85,7 +86,7 @@ export class WorkNoteService {
|
||||
userId: user.userId,
|
||||
userName: user.name || null,
|
||||
userRole: user.role || null, // Store participant type (INITIATOR/APPROVER/SPECTATOR)
|
||||
message: payload.message,
|
||||
message: sanitizeHtml(payload.message),
|
||||
isPriority: !!payload.isPriority,
|
||||
parentNoteId: payload.parentNoteId || null,
|
||||
mentionedUsers: payload.mentionedUsers || null,
|
||||
|
||||
27
src/types/xss.d.ts
vendored
Normal file
27
src/types/xss.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
declare module 'xss' {
|
||||
export interface IFilterXSSOptions {
|
||||
whiteList?: IWhiteList;
|
||||
onTag?: (tag: string, html: string, options: any) => string;
|
||||
onTagAttr?: (tag: string, name: string, value: string, isWhiteAttr: boolean) => string;
|
||||
onIgnoreTag?: (tag: string, html: string, options: any) => string;
|
||||
onIgnoreTagAttr?: (tag: string, name: string, value: string, isWhiteAttr: boolean) => string;
|
||||
escapeHtml?: (html: string) => string;
|
||||
stripIgnoreTag?: boolean;
|
||||
stripIgnoreTagBody?: boolean | string[];
|
||||
allowCommentTag?: boolean;
|
||||
}
|
||||
|
||||
export interface IWhiteList {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
export const whiteList: IWhiteList;
|
||||
|
||||
export class FilterXSS {
|
||||
constructor(options?: IFilterXSSOptions);
|
||||
process(html: string): string;
|
||||
}
|
||||
|
||||
export function filterXSS(html: string, options?: IFilterXSSOptions): string;
|
||||
export default filterXSS;
|
||||
}
|
||||
71
src/utils/sanitizer.ts
Normal file
71
src/utils/sanitizer.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { FilterXSS, whiteList } from 'xss';
|
||||
|
||||
/**
|
||||
* Sanitizes HTML content to prevent XSS attacks while allowing safe tags and attributes.
|
||||
* This is particularly important for content rendered with dangerouslySetInnerHTML.
|
||||
*
|
||||
* @param html The raw HTML string to sanitize
|
||||
* @returns The sanitized HTML string
|
||||
*/
|
||||
export const sanitizeHtml = (html: string): string => {
|
||||
if (!html) return '';
|
||||
|
||||
// Custom options can be added here if we need to allow specific tags or attributes
|
||||
// For now, using default options which are quite secure
|
||||
const options = {
|
||||
whiteList: {
|
||||
...whiteList,
|
||||
// Add any specific tags or attributes required by the frontend
|
||||
'span': ['style', 'class'],
|
||||
'div': ['style', 'class'],
|
||||
'p': ['style', 'class'],
|
||||
'br': [],
|
||||
'b': [],
|
||||
'i': [],
|
||||
'u': [],
|
||||
'strong': [],
|
||||
'em': [],
|
||||
'ul': ['style', 'class'],
|
||||
'ol': ['style', 'class'],
|
||||
'li': ['style', 'class'],
|
||||
'h1': ['style', 'class'],
|
||||
'h2': ['style', 'class'],
|
||||
'h3': ['style', 'class'],
|
||||
'h4': ['style', 'class'],
|
||||
'h5': ['style', 'class'],
|
||||
'h6': ['style', 'class'],
|
||||
'blockquote': ['style', 'class'],
|
||||
}
|
||||
};
|
||||
|
||||
const xssFilter = new FilterXSS(options);
|
||||
return xssFilter.process(html);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitizes an object by recursively sanitizing all string properties.
|
||||
* Useful for sanitizing request bodies or complex nested structures.
|
||||
*
|
||||
* @param obj The object to sanitize
|
||||
* @returns The sanitized object
|
||||
*/
|
||||
export const sanitizeObject = <T>(obj: T): T => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => sanitizeObject(item)) as any;
|
||||
}
|
||||
|
||||
const sanitized: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (typeof value === 'string') {
|
||||
sanitized[key] = sanitizeHtml(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
sanitized[key] = sanitizeObject(value);
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized as T;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user