secure-coding-rules 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bin/cli.js +8 -0
- package/package.json +64 -0
- package/src/adapters/agents.js +54 -0
- package/src/adapters/claude.js +88 -0
- package/src/adapters/copilot.js +82 -0
- package/src/adapters/cursor.js +68 -0
- package/src/adapters/windsurf.js +57 -0
- package/src/index.js +177 -0
- package/src/loader.js +85 -0
- package/src/prompts.js +307 -0
- package/src/templates/core/access-control.md +112 -0
- package/src/templates/core/authentication.md +138 -0
- package/src/templates/core/cryptographic.md +114 -0
- package/src/templates/core/data-integrity.md +144 -0
- package/src/templates/core/error-handling.md +180 -0
- package/src/templates/core/injection.md +125 -0
- package/src/templates/core/logging-alerting.md +156 -0
- package/src/templates/core/secure-design.md +117 -0
- package/src/templates/core/security-config.md +118 -0
- package/src/templates/core/supply-chain.md +127 -0
- package/src/templates/frontend/csp.md +146 -0
- package/src/templates/frontend/csrf-protection.md +151 -0
- package/src/templates/frontend/secure-state.md +167 -0
- package/src/templates/frontend/xss-prevention.md +125 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Content Security Policy Rules
|
|
2
|
+
|
|
3
|
+
> Frontend Security - Content Security Policy (CSP)
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Implement a Strict CSP
|
|
8
|
+
- **DO**: Define a Content-Security-Policy header that restricts script sources. Start with `default-src 'self'` and add specific directives as needed.
|
|
9
|
+
- **DON'T**: Use a permissive CSP (`default-src *`) or skip CSP entirely.
|
|
10
|
+
- **WHY**: CSP is the strongest browser-level defense against XSS. A strict policy prevents execution of injected scripts even if an XSS vulnerability exists.
|
|
11
|
+
|
|
12
|
+
### 2. Use Nonce-Based or Hash-Based Script Loading
|
|
13
|
+
- **DO**: Generate a unique cryptographic nonce per request and apply it to all `<script>` tags via `nonce` attribute. Set `script-src 'nonce-<value>'` in the CSP header.
|
|
14
|
+
- **DON'T**: Use `'unsafe-inline'` or `'unsafe-eval'` in `script-src`.
|
|
15
|
+
- **WHY**: `'unsafe-inline'` defeats the purpose of CSP. Nonces ensure only server-generated scripts execute.
|
|
16
|
+
|
|
17
|
+
### 3. Restrict Object and Frame Sources
|
|
18
|
+
- **DO**: Set `object-src 'none'` and `frame-ancestors 'self'` (or specific origins) to prevent plugin abuse and clickjacking.
|
|
19
|
+
- **DON'T**: Allow unrestricted embedding of your site in iframes or loading of Flash/Java plugins.
|
|
20
|
+
- **WHY**: Plugins can bypass other security controls. Unrestricted framing enables clickjacking attacks.
|
|
21
|
+
|
|
22
|
+
### 4. Enable CSP Reporting
|
|
23
|
+
- **DO**: Set `report-uri` or `report-to` directives to collect CSP violation reports. Monitor them for XSS attempts and policy issues.
|
|
24
|
+
- **DON'T**: Deploy CSP without monitoring. Use `Content-Security-Policy-Report-Only` for testing before enforcement.
|
|
25
|
+
- **WHY**: CSP reports reveal real attack attempts and policy misconfigurations without breaking the application.
|
|
26
|
+
|
|
27
|
+
### 5. Avoid Overly Broad Whitelists
|
|
28
|
+
- **DO**: Whitelist specific origins (e.g., `script-src 'self' https://cdn.specific.com`). Use `strict-dynamic` with nonces for dynamically loaded scripts.
|
|
29
|
+
- **DON'T**: Whitelist entire CDN domains like `*.cloudflare.com` or `*.googleapis.com` that host arbitrary user content.
|
|
30
|
+
- **WHY**: Broad domain whitelists can be bypassed using JSONP endpoints or user-uploaded scripts on those domains.
|
|
31
|
+
|
|
32
|
+
### 6. Apply CSP to All Response Types
|
|
33
|
+
- **DO**: Set CSP headers on HTML responses, error pages, and API responses that might be rendered by browsers.
|
|
34
|
+
- **DON'T**: Only apply CSP to your main pages, leaving error pages, login pages, or admin panels unprotected.
|
|
35
|
+
- **WHY**: Attackers target pages without CSP protection. Consistent application ensures no gaps.
|
|
36
|
+
|
|
37
|
+
## Code Examples
|
|
38
|
+
|
|
39
|
+
### Bad Practice
|
|
40
|
+
```javascript
|
|
41
|
+
// Permissive CSP that provides minimal protection
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
res.setHeader("Content-Security-Policy", "default-src *; script-src * 'unsafe-inline' 'unsafe-eval'");
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Inline script without nonce (blocked by strict CSP)
|
|
48
|
+
// <script>doSomething();</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Good Practice
|
|
52
|
+
```javascript
|
|
53
|
+
import crypto from "node:crypto";
|
|
54
|
+
|
|
55
|
+
// Strict nonce-based CSP middleware
|
|
56
|
+
function cspMiddleware(req, res, next) {
|
|
57
|
+
const nonce = crypto.randomBytes(16).toString("base64");
|
|
58
|
+
res.locals.cspNonce = nonce;
|
|
59
|
+
|
|
60
|
+
const csp = [
|
|
61
|
+
`default-src 'self'`,
|
|
62
|
+
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
|
|
63
|
+
`style-src 'self' 'nonce-${nonce}'`,
|
|
64
|
+
`img-src 'self' data: https:`,
|
|
65
|
+
`font-src 'self'`,
|
|
66
|
+
`connect-src 'self' https://api.example.com`,
|
|
67
|
+
`frame-ancestors 'self'`,
|
|
68
|
+
`object-src 'none'`,
|
|
69
|
+
`base-uri 'self'`,
|
|
70
|
+
`form-action 'self'`,
|
|
71
|
+
`report-uri /csp-report`,
|
|
72
|
+
].join("; ");
|
|
73
|
+
|
|
74
|
+
res.setHeader("Content-Security-Policy", csp);
|
|
75
|
+
next();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
app.use(cspMiddleware);
|
|
79
|
+
|
|
80
|
+
// CSP violation report endpoint
|
|
81
|
+
app.post("/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
|
|
82
|
+
const report = req.body["csp-report"];
|
|
83
|
+
logger.warn({
|
|
84
|
+
type: "csp_violation",
|
|
85
|
+
blockedUri: report["blocked-uri"],
|
|
86
|
+
violatedDirective: report["violated-directive"],
|
|
87
|
+
documentUri: report["document-uri"],
|
|
88
|
+
sourceFile: report["source-file"],
|
|
89
|
+
lineNumber: report["line-number"],
|
|
90
|
+
});
|
|
91
|
+
res.sendStatus(204);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// HTML template using nonce
|
|
95
|
+
function renderPage(res, content) {
|
|
96
|
+
return `
|
|
97
|
+
<!DOCTYPE html>
|
|
98
|
+
<html>
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="utf-8">
|
|
101
|
+
<link rel="stylesheet" href="/styles/main.css" nonce="${res.locals.cspNonce}">
|
|
102
|
+
</head>
|
|
103
|
+
<body>
|
|
104
|
+
${content}
|
|
105
|
+
<script nonce="${res.locals.cspNonce}" src="/js/app.js"></script>
|
|
106
|
+
</body>
|
|
107
|
+
</html>
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Next.js CSP configuration (next.config.js)
|
|
112
|
+
const nextConfig = {
|
|
113
|
+
async headers() {
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
source: "/(.*)",
|
|
117
|
+
headers: [
|
|
118
|
+
{
|
|
119
|
+
key: "Content-Security-Policy",
|
|
120
|
+
value: [
|
|
121
|
+
"default-src 'self'",
|
|
122
|
+
"script-src 'self' 'unsafe-inline'", // Next.js requires unsafe-inline; use nonce in custom server
|
|
123
|
+
"style-src 'self' 'unsafe-inline'",
|
|
124
|
+
"img-src 'self' data: https:",
|
|
125
|
+
"font-src 'self'",
|
|
126
|
+
"object-src 'none'",
|
|
127
|
+
"frame-ancestors 'self'",
|
|
128
|
+
].join("; "),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Quick Checklist
|
|
138
|
+
- [ ] CSP header set on all HTML responses
|
|
139
|
+
- [ ] `default-src 'self'` as baseline
|
|
140
|
+
- [ ] `script-src` uses nonces or hashes (no `'unsafe-inline'` or `'unsafe-eval'`)
|
|
141
|
+
- [ ] `object-src 'none'` set to block plugins
|
|
142
|
+
- [ ] `frame-ancestors` set to prevent clickjacking
|
|
143
|
+
- [ ] `base-uri 'self'` to prevent base tag hijacking
|
|
144
|
+
- [ ] CSP reporting configured and monitored
|
|
145
|
+
- [ ] `Content-Security-Policy-Report-Only` used for testing before enforcement
|
|
146
|
+
- [ ] No overly broad CDN domain whitelists
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# CSRF Protection Security Rules
|
|
2
|
+
|
|
3
|
+
> Frontend Security - Cross-Site Request Forgery (CSRF) Defense
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Implement Anti-CSRF Tokens
|
|
8
|
+
- **DO**: Generate a unique, unpredictable CSRF token per session or per request. Include it in every state-changing request.
|
|
9
|
+
- **DON'T**: Rely on cookies alone for authentication of state-changing requests.
|
|
10
|
+
- **WHY**: CSRF attacks exploit the browser's automatic cookie inclusion. Tokens add a second factor that attackers cannot obtain cross-origin.
|
|
11
|
+
|
|
12
|
+
### 2. Use SameSite Cookie Attribute
|
|
13
|
+
- **DO**: Set `SameSite=Strict` for session cookies where possible, or `SameSite=Lax` as a minimum.
|
|
14
|
+
- **DON'T**: Use `SameSite=None` without `Secure` flag, or omit the SameSite attribute entirely.
|
|
15
|
+
- **WHY**: SameSite prevents browsers from sending cookies with cross-origin requests, providing a strong defense-in-depth layer.
|
|
16
|
+
|
|
17
|
+
### 3. Verify Origin and Referer Headers
|
|
18
|
+
- **DO**: On the server, validate `Origin` or `Referer` headers match your domain for state-changing requests.
|
|
19
|
+
- **DON'T**: Accept requests without origin validation, especially for API endpoints that modify data.
|
|
20
|
+
- **WHY**: Origin/Referer verification is a supplementary defense that blocks cross-origin form submissions and fetch requests.
|
|
21
|
+
|
|
22
|
+
### 4. Use Custom Request Headers for APIs
|
|
23
|
+
- **DO**: Require a custom header (e.g., `X-Requested-With`) for all API requests. Simple forms cannot set custom headers.
|
|
24
|
+
- **DON'T**: Accept state-changing requests that could originate from simple HTML forms without additional verification.
|
|
25
|
+
- **WHY**: Browsers enforce that cross-origin requests with custom headers trigger a CORS preflight, which fails unless explicitly allowed.
|
|
26
|
+
|
|
27
|
+
### 5. Separate Read and Write Operations
|
|
28
|
+
- **DO**: Use GET only for read operations. Use POST, PUT, PATCH, DELETE for state-changing operations.
|
|
29
|
+
- **DON'T**: Use GET requests for actions that modify data (e.g., `/api/delete-account?confirm=true`).
|
|
30
|
+
- **WHY**: GET requests can be triggered by `<img>` tags, links, and redirects, making them trivially exploitable for CSRF.
|
|
31
|
+
|
|
32
|
+
### 6. Implement Double-Submit Cookie Pattern When Stateless
|
|
33
|
+
- **DO**: Set a random value in a cookie and require the same value in a request header or body. Compare both server-side.
|
|
34
|
+
- **DON'T**: Use a predictable or static value for the double-submit token.
|
|
35
|
+
- **WHY**: This pattern works without server-side token storage, suitable for stateless APIs and SPAs.
|
|
36
|
+
|
|
37
|
+
## Code Examples
|
|
38
|
+
|
|
39
|
+
### Bad Practice
|
|
40
|
+
```javascript
|
|
41
|
+
// State-changing GET request
|
|
42
|
+
app.get("/api/transfer", authenticate, async (req, res) => {
|
|
43
|
+
await transferFunds(req.user.id, req.query.to, req.query.amount);
|
|
44
|
+
res.json({ success: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// No CSRF protection on form submission
|
|
48
|
+
app.post("/api/change-email", authenticate, async (req, res) => {
|
|
49
|
+
await db.updateEmail(req.user.id, req.body.email);
|
|
50
|
+
res.json({ success: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// SameSite not set
|
|
54
|
+
res.cookie("session", token, { httpOnly: true, secure: true }); // Missing SameSite
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Good Practice
|
|
58
|
+
```javascript
|
|
59
|
+
import crypto from "node:crypto";
|
|
60
|
+
|
|
61
|
+
// CSRF token generation and validation middleware
|
|
62
|
+
function csrfProtection() {
|
|
63
|
+
return (req, res, next) => {
|
|
64
|
+
// Generate token for GET requests
|
|
65
|
+
if (req.method === "GET") {
|
|
66
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
67
|
+
req.session.csrfToken = token;
|
|
68
|
+
res.locals.csrfToken = token;
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate token for state-changing requests
|
|
73
|
+
const token = req.headers["x-csrf-token"] ?? req.body._csrf;
|
|
74
|
+
if (!token || token !== req.session.csrfToken) {
|
|
75
|
+
return res.status(403).json({ error: "Invalid CSRF token" });
|
|
76
|
+
}
|
|
77
|
+
next();
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Apply CSRF protection
|
|
82
|
+
app.use(csrfProtection());
|
|
83
|
+
|
|
84
|
+
// Secure cookie with SameSite
|
|
85
|
+
res.cookie("session", token, {
|
|
86
|
+
httpOnly: true,
|
|
87
|
+
secure: true,
|
|
88
|
+
sameSite: "strict",
|
|
89
|
+
maxAge: 3600_000,
|
|
90
|
+
path: "/",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Origin validation middleware
|
|
94
|
+
function validateOrigin(allowedOrigins) {
|
|
95
|
+
return (req, res, next) => {
|
|
96
|
+
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next();
|
|
97
|
+
const origin = req.headers.origin ?? req.headers.referer;
|
|
98
|
+
if (!origin) {
|
|
99
|
+
return res.status(403).json({ error: "Missing origin header" });
|
|
100
|
+
}
|
|
101
|
+
const requestOrigin = new URL(origin).origin;
|
|
102
|
+
if (!allowedOrigins.includes(requestOrigin)) {
|
|
103
|
+
return res.status(403).json({ error: "Invalid origin" });
|
|
104
|
+
}
|
|
105
|
+
next();
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
app.use(validateOrigin(["https://myapp.example.com"]));
|
|
110
|
+
|
|
111
|
+
// Frontend: Include CSRF token in requests
|
|
112
|
+
async function securePost(url, data) {
|
|
113
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
114
|
+
return fetch(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
credentials: "same-origin",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
"X-CSRF-Token": csrfToken,
|
|
120
|
+
"X-Requested-With": "fetch",
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify(data),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Double-submit cookie pattern for SPAs
|
|
127
|
+
function doubleSubmitCsrf() {
|
|
128
|
+
return (req, res, next) => {
|
|
129
|
+
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
|
130
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
131
|
+
res.cookie("csrf", token, { sameSite: "strict", secure: true });
|
|
132
|
+
return next();
|
|
133
|
+
}
|
|
134
|
+
const cookieToken = req.cookies.csrf;
|
|
135
|
+
const headerToken = req.headers["x-csrf-token"];
|
|
136
|
+
if (!cookieToken || cookieToken !== headerToken) {
|
|
137
|
+
return res.status(403).json({ error: "CSRF validation failed" });
|
|
138
|
+
}
|
|
139
|
+
next();
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Quick Checklist
|
|
145
|
+
- [ ] CSRF tokens included in all state-changing requests
|
|
146
|
+
- [ ] Session cookies set with `SameSite=Strict` or `SameSite=Lax`
|
|
147
|
+
- [ ] `Origin` / `Referer` headers validated server-side
|
|
148
|
+
- [ ] GET requests never used for state-changing operations
|
|
149
|
+
- [ ] Custom headers required for API requests (`X-Requested-With`)
|
|
150
|
+
- [ ] CSRF tokens are cryptographically random and per-session
|
|
151
|
+
- [ ] Frontend fetch/axios includes CSRF token in headers
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Secure State Management and DOM Security Rules
|
|
2
|
+
|
|
3
|
+
> Frontend Security - Safe State Management and DOM Manipulation
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Never Store Sensitive Data in Client-Side State
|
|
8
|
+
- **DO**: Store only non-sensitive UI state in the browser. Keep tokens in `HttpOnly` cookies managed by the server.
|
|
9
|
+
- **DON'T**: Store JWTs, API keys, passwords, or PII in `localStorage`, `sessionStorage`, Redux/Zustand stores, or global variables.
|
|
10
|
+
- **WHY**: All client-side storage is accessible via XSS. A single XSS vulnerability exposes all data in `localStorage` and JavaScript state.
|
|
11
|
+
|
|
12
|
+
### 2. Sanitize Data Before DOM Insertion
|
|
13
|
+
- **DO**: Use `textContent` for text, framework bindings for dynamic content, and DOMPurify for user-generated HTML.
|
|
14
|
+
- **DON'T**: Use `innerHTML`, `outerHTML`, `document.write()`, or jQuery's `.html()` with any user-influenced data.
|
|
15
|
+
- **WHY**: Unsafe DOM APIs parse strings as HTML, enabling XSS through injected `<script>` tags, event handlers, or malicious attributes.
|
|
16
|
+
|
|
17
|
+
### 3. Validate postMessage Communication
|
|
18
|
+
- **DO**: Always check `event.origin` against an allowlist. Validate the message schema before processing.
|
|
19
|
+
- **DON'T**: Accept messages from any origin or use `targetOrigin: "*"` when sending sensitive data.
|
|
20
|
+
- **WHY**: Without origin validation, any page can send messages to your application, enabling XSS and data injection attacks.
|
|
21
|
+
|
|
22
|
+
### 4. Avoid Exposing Sensitive State in URLs
|
|
23
|
+
- **DO**: Use POST requests for sensitive data. Keep tokens and PII out of URL parameters and fragments.
|
|
24
|
+
- **DON'T**: Pass tokens, session IDs, or sensitive data in URL query strings or hash fragments.
|
|
25
|
+
- **WHY**: URLs are logged in browser history, server logs, referrer headers, and analytics tools, leaking sensitive data.
|
|
26
|
+
|
|
27
|
+
### 5. Implement Immutable State Updates
|
|
28
|
+
- **DO**: Use immutable update patterns (spread operator, `structuredClone()`, Immer). Never mutate state directly.
|
|
29
|
+
- **DON'T**: Modify state objects directly or share mutable references between components.
|
|
30
|
+
- **WHY**: Mutable state causes unpredictable UI updates, race conditions, and can mask security-relevant state changes (e.g., permission levels).
|
|
31
|
+
|
|
32
|
+
### 6. Secure Third-Party Widget Integration
|
|
33
|
+
- **DO**: Load third-party widgets in sandboxed `<iframe>` elements with minimal `allow` attributes. Use `sandbox` attribute.
|
|
34
|
+
- **DON'T**: Load third-party scripts directly into your page's DOM context.
|
|
35
|
+
- **WHY**: Third-party scripts have full access to the page's DOM, cookies, and state. A compromised widget compromises your entire application.
|
|
36
|
+
|
|
37
|
+
### 7. Clear Sensitive State on Logout and Navigation
|
|
38
|
+
- **DO**: Clear all sensitive in-memory state, revoke tokens, and invalidate sessions when users log out.
|
|
39
|
+
- **DON'T**: Leave sensitive data in memory or storage after logout or session expiry.
|
|
40
|
+
- **WHY**: Residual state can be accessed by subsequent users on shared devices or by malicious scripts after session expiry.
|
|
41
|
+
|
|
42
|
+
## Code Examples
|
|
43
|
+
|
|
44
|
+
### Bad Practice
|
|
45
|
+
```javascript
|
|
46
|
+
// Storing JWT in localStorage
|
|
47
|
+
localStorage.setItem("authToken", response.token);
|
|
48
|
+
|
|
49
|
+
// Direct DOM manipulation with user data
|
|
50
|
+
document.getElementById("bio").innerHTML = user.bio;
|
|
51
|
+
|
|
52
|
+
// postMessage without origin check
|
|
53
|
+
window.addEventListener("message", (event) => {
|
|
54
|
+
updateProfile(event.data); // Accepts from any origin
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Sending sensitive data via postMessage to any origin
|
|
58
|
+
parent.postMessage({ token: authToken }, "*");
|
|
59
|
+
|
|
60
|
+
// Sensitive data in URL
|
|
61
|
+
window.location.href = `/dashboard?token=${authToken}&ssn=${user.ssn}`;
|
|
62
|
+
|
|
63
|
+
// Mutating state directly
|
|
64
|
+
state.user.role = "admin"; // Direct mutation
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Good Practice
|
|
68
|
+
```javascript
|
|
69
|
+
// Auth tokens in HttpOnly cookies (set by server, not accessible via JS)
|
|
70
|
+
// Server-side:
|
|
71
|
+
res.cookie("session", sessionToken, {
|
|
72
|
+
httpOnly: true,
|
|
73
|
+
secure: true,
|
|
74
|
+
sameSite: "strict",
|
|
75
|
+
maxAge: 3600_000,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Client-side: cookies are sent automatically, no JS storage needed
|
|
79
|
+
async function fetchProfile() {
|
|
80
|
+
return fetch("/api/profile", { credentials: "same-origin" });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Safe DOM update
|
|
84
|
+
document.getElementById("bio").textContent = user.bio;
|
|
85
|
+
|
|
86
|
+
// Secure postMessage with origin validation
|
|
87
|
+
const TRUSTED_ORIGINS = new Set(["https://trusted-widget.example.com"]);
|
|
88
|
+
|
|
89
|
+
window.addEventListener("message", (event) => {
|
|
90
|
+
if (!TRUSTED_ORIGINS.has(event.origin)) return;
|
|
91
|
+
const schema = z.object({ type: z.string(), payload: z.unknown() });
|
|
92
|
+
const result = schema.safeParse(event.data);
|
|
93
|
+
if (!result.success) return;
|
|
94
|
+
handleValidatedMessage(result.data);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Send with specific target origin
|
|
98
|
+
widgetFrame.contentWindow.postMessage(
|
|
99
|
+
{ type: "theme", value: "dark" },
|
|
100
|
+
"https://trusted-widget.example.com" // Specific origin, not "*"
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Immutable state updates (React example)
|
|
104
|
+
function userReducer(state, action) {
|
|
105
|
+
switch (action.type) {
|
|
106
|
+
case "UPDATE_PROFILE":
|
|
107
|
+
return { ...state, profile: { ...state.profile, ...action.payload } };
|
|
108
|
+
case "LOGOUT":
|
|
109
|
+
return { ...initialState }; // Reset to clean state
|
|
110
|
+
default:
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Sandboxed third-party widget
|
|
116
|
+
function ThirdPartyWidget({ src }) {
|
|
117
|
+
return (
|
|
118
|
+
<iframe
|
|
119
|
+
src={src}
|
|
120
|
+
sandbox="allow-scripts allow-same-origin"
|
|
121
|
+
referrerPolicy="no-referrer"
|
|
122
|
+
loading="lazy"
|
|
123
|
+
style={{ border: "none" }}
|
|
124
|
+
title="Widget"
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Comprehensive logout
|
|
130
|
+
async function secureLogout() {
|
|
131
|
+
await fetch("/api/logout", { method: "POST", credentials: "same-origin" });
|
|
132
|
+
// Clear any client-side state
|
|
133
|
+
sessionStorage.clear();
|
|
134
|
+
// Reset application state
|
|
135
|
+
store.dispatch({ type: "RESET" });
|
|
136
|
+
// Navigate to prevent back-button access
|
|
137
|
+
window.location.replace("/login");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Clear sensitive data from memory (for sensitive forms)
|
|
141
|
+
function SecureForm() {
|
|
142
|
+
const [formData, setFormData] = useState({ cardNumber: "", cvv: "" });
|
|
143
|
+
|
|
144
|
+
async function handleSubmit(e) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
try {
|
|
147
|
+
await submitPayment(formData);
|
|
148
|
+
} finally {
|
|
149
|
+
setFormData({ cardNumber: "", cvv: "" }); // Clear immediately after use
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Quick Checklist
|
|
158
|
+
- [ ] No JWTs, API keys, or PII in `localStorage` or `sessionStorage`
|
|
159
|
+
- [ ] Auth tokens stored in `HttpOnly`, `Secure`, `SameSite` cookies
|
|
160
|
+
- [ ] `textContent` used instead of `innerHTML` for user data
|
|
161
|
+
- [ ] `postMessage` handlers validate `event.origin` against allowlist
|
|
162
|
+
- [ ] `postMessage` sends specify exact `targetOrigin` (not `"*"`)
|
|
163
|
+
- [ ] No sensitive data in URL parameters or hash fragments
|
|
164
|
+
- [ ] Immutable state update patterns used consistently
|
|
165
|
+
- [ ] Third-party widgets loaded in sandboxed iframes
|
|
166
|
+
- [ ] All sensitive state cleared on logout
|
|
167
|
+
- [ ] Shared device considerations addressed (no residual state)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# XSS Prevention Security Rules
|
|
2
|
+
|
|
3
|
+
> Frontend Security - Cross-Site Scripting (XSS) Defense
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
### 1. Never Use `innerHTML` or `dangerouslySetInnerHTML` with User Input
|
|
8
|
+
- **DO**: Use `textContent` for plain text insertion. Use a trusted sanitization library (DOMPurify) when HTML rendering is absolutely necessary.
|
|
9
|
+
- **DON'T**: Assign user-controlled strings to `innerHTML`, `outerHTML`, or React's `dangerouslySetInnerHTML`.
|
|
10
|
+
- **WHY**: Direct HTML insertion is the primary XSS vector. Any unsanitized markup can execute arbitrary JavaScript in the user's browser.
|
|
11
|
+
|
|
12
|
+
### 2. Leverage Framework Auto-Escaping
|
|
13
|
+
- **DO**: Use React JSX expressions `{variable}`, Vue template interpolation `{{ variable }}`, or Angular template binding `{{ variable }}` which auto-escape by default.
|
|
14
|
+
- **DON'T**: Bypass framework escaping mechanisms unless absolutely necessary and with sanitized input.
|
|
15
|
+
- **WHY**: Modern frameworks escape output by default, preventing the majority of XSS attacks without extra effort.
|
|
16
|
+
|
|
17
|
+
### 3. Sanitize Rich Text and Markdown
|
|
18
|
+
- **DO**: Use DOMPurify with a strict allowlist of tags and attributes when rendering user-generated HTML or Markdown.
|
|
19
|
+
- **DON'T**: Trust Markdown-to-HTML conversion output without post-sanitization. Never allow `<script>`, `<iframe>`, event handlers, or `javascript:` URIs.
|
|
20
|
+
- **WHY**: Markdown parsers may produce raw HTML. Even "safe" tags can carry malicious attributes like `onerror` or `onload`.
|
|
21
|
+
|
|
22
|
+
### 4. Validate and Sanitize URLs
|
|
23
|
+
- **DO**: Validate that user-provided URLs use `https:` or `http:` protocols. Reject `javascript:`, `data:`, and `vbscript:` URIs.
|
|
24
|
+
- **DON'T**: Render user-supplied URLs in `href`, `src`, or `action` attributes without protocol validation.
|
|
25
|
+
- **WHY**: `javascript:` URIs execute code when clicked or loaded, bypassing typical XSS defenses.
|
|
26
|
+
|
|
27
|
+
### 5. Avoid Dynamic Code Execution
|
|
28
|
+
- **DO**: Use static templates and data binding. Parse JSON with `JSON.parse()`.
|
|
29
|
+
- **DON'T**: Use `eval()`, `Function()`, `setTimeout(string)`, or `setInterval(string)` with any data derived from user input.
|
|
30
|
+
- **WHY**: Dynamic code execution converts data into code, the fundamental cause of injection attacks.
|
|
31
|
+
|
|
32
|
+
### 6. Escape Data in Non-HTML Contexts
|
|
33
|
+
- **DO**: Apply context-specific encoding when inserting data into JavaScript strings, CSS values, or URL parameters.
|
|
34
|
+
- **DON'T**: Assume HTML escaping is sufficient for all contexts. Each context (JS, CSS, URL) needs its own encoding.
|
|
35
|
+
- **WHY**: HTML-encoded data inserted into a `<script>` block or inline CSS can still execute as code.
|
|
36
|
+
|
|
37
|
+
### 7. Implement DOM-Based XSS Prevention
|
|
38
|
+
- **DO**: Treat all client-side data sources (URL parameters, `location.hash`, `document.referrer`, `postMessage`) as untrusted. Validate before use.
|
|
39
|
+
- **DON'T**: Read from `window.location`, `document.cookie`, or `localStorage` and insert directly into the DOM.
|
|
40
|
+
- **WHY**: DOM-based XSS occurs entirely in the browser without server involvement, bypassing server-side sanitization.
|
|
41
|
+
|
|
42
|
+
## Code Examples
|
|
43
|
+
|
|
44
|
+
### Bad Practice
|
|
45
|
+
```javascript
|
|
46
|
+
// Direct innerHTML with user input
|
|
47
|
+
document.getElementById("output").innerHTML = userComment;
|
|
48
|
+
|
|
49
|
+
// React dangerouslySetInnerHTML without sanitization
|
|
50
|
+
function Comment({ body }) {
|
|
51
|
+
return <div dangerouslySetInnerHTML={{ __html: body }} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// javascript: URI in href
|
|
55
|
+
function UserLink({ url, label }) {
|
|
56
|
+
return <a href={url}>{label}</a>; // url could be "javascript:alert(1)"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// DOM-based XSS via URL hash
|
|
60
|
+
const name = decodeURIComponent(window.location.hash.slice(1));
|
|
61
|
+
document.getElementById("greeting").innerHTML = `Hello, ${name}!`;
|
|
62
|
+
|
|
63
|
+
// eval with user data
|
|
64
|
+
const config = eval(`(${searchParams.get("config")})`);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Good Practice
|
|
68
|
+
```javascript
|
|
69
|
+
import DOMPurify from "dompurify";
|
|
70
|
+
|
|
71
|
+
// Safe text insertion
|
|
72
|
+
document.getElementById("output").textContent = userComment;
|
|
73
|
+
|
|
74
|
+
// React with DOMPurify for rich content
|
|
75
|
+
function Comment({ body }) {
|
|
76
|
+
const sanitized = DOMPurify.sanitize(body, {
|
|
77
|
+
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li"],
|
|
78
|
+
ALLOWED_ATTR: ["href", "title"],
|
|
79
|
+
});
|
|
80
|
+
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Safe URL validation
|
|
84
|
+
function isValidUrl(url) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = new URL(url);
|
|
87
|
+
return ["https:", "http:"].includes(parsed.protocol);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function UserLink({ url, label }) {
|
|
94
|
+
const safeUrl = isValidUrl(url) ? url : "#";
|
|
95
|
+
return <a href={safeUrl} rel="noopener noreferrer">{label}</a>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Safe DOM-based rendering
|
|
99
|
+
const name = decodeURIComponent(window.location.hash.slice(1));
|
|
100
|
+
const greeting = document.getElementById("greeting");
|
|
101
|
+
greeting.textContent = `Hello, ${name}!`; // textContent, not innerHTML
|
|
102
|
+
|
|
103
|
+
// Safe postMessage handling
|
|
104
|
+
window.addEventListener("message", (event) => {
|
|
105
|
+
if (event.origin !== "https://trusted-domain.com") return; // Origin check
|
|
106
|
+
const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
|
|
107
|
+
// Validate schema before use
|
|
108
|
+
if (typeof data.action === "string" && typeof data.value === "string") {
|
|
109
|
+
handleMessage(data);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Safe JSON parsing instead of eval
|
|
114
|
+
const config = JSON.parse(searchParams.get("config") ?? "{}");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Quick Checklist
|
|
118
|
+
- [ ] No `innerHTML` or `dangerouslySetInnerHTML` with unsanitized user input
|
|
119
|
+
- [ ] DOMPurify used for any user-generated HTML/Markdown rendering
|
|
120
|
+
- [ ] Framework auto-escaping relied upon for standard output
|
|
121
|
+
- [ ] User-provided URLs validated for safe protocols (`https:`, `http:`)
|
|
122
|
+
- [ ] No `eval()`, `Function()`, or `setTimeout(string)` with user data
|
|
123
|
+
- [ ] `postMessage` handlers validate `event.origin`
|
|
124
|
+
- [ ] URL parameters, hash, and referrer treated as untrusted input
|
|
125
|
+
- [ ] Context-specific encoding applied (HTML, JS, CSS, URL contexts)
|