@schalkneethling/calavera-skill-frontend-security 0.2.0-next.2
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/calavera-artifact.json +11 -0
- package/package.json +24 -0
- package/payload/frontend-security/SKILL.md +57 -0
- package/payload/frontend-security/agents/openai.yaml +4 -0
- package/payload/frontend-security/references/audit-workflow.md +57 -0
- package/payload/frontend-security/references/csp-configuration.md +262 -0
- package/payload/frontend-security/references/csrf-protection.md +383 -0
- package/payload/frontend-security/references/dom-security.md +263 -0
- package/payload/frontend-security/references/file-upload-security.md +662 -0
- package/payload/frontend-security/references/framework-patterns.md +328 -0
- package/payload/frontend-security/references/input-validation.md +360 -0
- package/payload/frontend-security/references/jwt-security.md +352 -0
- package/payload/frontend-security/references/nodejs-npm-security.md +325 -0
- package/payload/frontend-security/references/xss-prevention.md +212 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# XSS Prevention Reference
|
|
2
|
+
|
|
3
|
+
## Output Encoding Rules
|
|
4
|
+
|
|
5
|
+
Apply context-appropriate encoding for all untrusted data:
|
|
6
|
+
|
|
7
|
+
| Context | Encoding Method | Example |
|
|
8
|
+
| -------------- | -------------------- | --------------------- |
|
|
9
|
+
| HTML Body | HTML Entity Encoding | `<script>` |
|
|
10
|
+
| HTML Attribute | Attribute Encoding | `"onclick"` |
|
|
11
|
+
| JavaScript | JavaScript Encoding | `\x3cscript\x3e` |
|
|
12
|
+
| CSS | CSS Encoding | `\3c script\3e` |
|
|
13
|
+
| URL Parameter | URL Encoding | `%3Cscript%3E` |
|
|
14
|
+
|
|
15
|
+
## Safe vs Unsafe Sinks
|
|
16
|
+
|
|
17
|
+
### Unsafe Sinks (Never use with untrusted data)
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
// Execution sinks - NEVER use with user input
|
|
21
|
+
element.innerHTML = userInput; // XSS
|
|
22
|
+
element.outerHTML = userInput; // XSS
|
|
23
|
+
document.write(userInput); // XSS
|
|
24
|
+
document.writeln(userInput); // XSS
|
|
25
|
+
|
|
26
|
+
// JavaScript execution sinks
|
|
27
|
+
eval(userInput); // XSS
|
|
28
|
+
new Function(userInput); // XSS
|
|
29
|
+
setTimeout(userInput, time); // XSS if string
|
|
30
|
+
setInterval(userInput, time); // XSS if string
|
|
31
|
+
|
|
32
|
+
// URL sinks
|
|
33
|
+
location.href = userInput; // XSS
|
|
34
|
+
location.assign(userInput); // XSS
|
|
35
|
+
location.replace(userInput); // XSS
|
|
36
|
+
window.open(userInput); // XSS
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Safe Alternatives
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Safe text insertion
|
|
43
|
+
element.textContent = userInput; // Safe
|
|
44
|
+
element.innerText = userInput; // Safe
|
|
45
|
+
|
|
46
|
+
// Safe attribute setting (only for explicitly benign attributes)
|
|
47
|
+
element.setAttribute("title", userInput); // Safe for plain text attributes
|
|
48
|
+
// Do not treat href, src, style, or event-handler attributes as safe sinks.
|
|
49
|
+
|
|
50
|
+
// Safe URL handling: parse, normalize, then enforce the actual policy.
|
|
51
|
+
function matchesPathPrefix(pathname, allowedPrefix) {
|
|
52
|
+
return pathname === allowedPrefix || pathname.startsWith(`${allowedPrefix}/`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toAllowedInternalUrl(input) {
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(input, window.location.origin);
|
|
58
|
+
if (url.origin !== window.location.origin) return null;
|
|
59
|
+
if (!["/account", "/settings", "/help"].some((path) => matchesPathPrefix(url.pathname, path))) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return url.href;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const safeUrl = toAllowedInternalUrl(userInput);
|
|
69
|
+
if (safeUrl) location.assign(safeUrl);
|
|
70
|
+
|
|
71
|
+
// New tabs/windows need opener protection when the destination is untrusted or
|
|
72
|
+
// external.
|
|
73
|
+
if (safeUrl) {
|
|
74
|
+
const opened = window.open(safeUrl, "_blank", "noopener,noreferrer");
|
|
75
|
+
if (opened) opened.opener = null;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## HTML Sanitization
|
|
80
|
+
|
|
81
|
+
When HTML must be rendered, use sanitization:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// Using DOMPurify (recommended)
|
|
85
|
+
import DOMPurify from "dompurify";
|
|
86
|
+
element.innerHTML = DOMPurify.sanitize(userInput);
|
|
87
|
+
|
|
88
|
+
// With configuration
|
|
89
|
+
const clean = DOMPurify.sanitize(dirty, {
|
|
90
|
+
ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
|
|
91
|
+
ALLOWED_ATTR: ["href", "title"],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Browser Sanitizer API (when available)
|
|
95
|
+
const sanitizer = new Sanitizer({
|
|
96
|
+
allowElements: ["b", "i", "em", "strong", "a"],
|
|
97
|
+
allowAttributes: { href: ["a"] },
|
|
98
|
+
});
|
|
99
|
+
element.setHTML(userInput, { sanitizer });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Framework-Specific XSS Risks
|
|
103
|
+
|
|
104
|
+
### React
|
|
105
|
+
|
|
106
|
+
```jsx
|
|
107
|
+
// DANGEROUS - bypasses React's protection
|
|
108
|
+
<div dangerouslySetInnerHTML={{ __html: userInput }} />
|
|
109
|
+
|
|
110
|
+
// SAFE - React auto-escapes
|
|
111
|
+
<div>{userInput}</div>
|
|
112
|
+
|
|
113
|
+
// DANGEROUS - href can contain javascript:, phishing, or redirect targets.
|
|
114
|
+
<a href={userInput}>Link</a>
|
|
115
|
+
|
|
116
|
+
// SAFER - parse and enforce the allowed destinations for this component.
|
|
117
|
+
function toSafeExternalHref(input) {
|
|
118
|
+
try {
|
|
119
|
+
const url = new URL(input, window.location.origin);
|
|
120
|
+
if (!["https:"].includes(url.protocol)) return "#";
|
|
121
|
+
if (!["https://example.com", "https://docs.example.com"].includes(url.origin)) return "#";
|
|
122
|
+
return url.href;
|
|
123
|
+
} catch {
|
|
124
|
+
return "#";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
<a href={toSafeExternalHref(userInput)} target="_blank" rel="noopener noreferrer">
|
|
129
|
+
Link
|
|
130
|
+
</a>;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Twig
|
|
134
|
+
|
|
135
|
+
```twig
|
|
136
|
+
{# DANGEROUS - raw filter bypasses escaping #}
|
|
137
|
+
{{ userInput|raw }}
|
|
138
|
+
|
|
139
|
+
{# DANGEROUS - autoescape disabled #}
|
|
140
|
+
{% autoescape false %}
|
|
141
|
+
{{ userInput }}
|
|
142
|
+
{% endautoescape %}
|
|
143
|
+
|
|
144
|
+
{# SAFE - auto-escaped by default #}
|
|
145
|
+
{{ userInput }}
|
|
146
|
+
|
|
147
|
+
{# SAFE - explicit escaping #}
|
|
148
|
+
{{ userInput|e('html') }}
|
|
149
|
+
{{ userInput|e('js') }}
|
|
150
|
+
{{ userInput|e('url') }}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Astro
|
|
154
|
+
|
|
155
|
+
```astro
|
|
156
|
+
<!-- DANGEROUS - set:html bypasses escaping -->
|
|
157
|
+
<div set:html={userInput} />
|
|
158
|
+
|
|
159
|
+
<!-- SAFE - auto-escaped -->
|
|
160
|
+
<div>{userInput}</div>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## URL Validation
|
|
164
|
+
|
|
165
|
+
URL checks should return a parsed, normalized value for the exact sink. A scheme
|
|
166
|
+
allowlist is necessary but not sufficient for redirects, links, downloads, or
|
|
167
|
+
navigation.
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
function matchesPathPrefix(pathname, allowedPrefix) {
|
|
171
|
+
if (allowedPrefix === "/") return pathname.startsWith("/");
|
|
172
|
+
return pathname === allowedPrefix || pathname.startsWith(`${allowedPrefix}/`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeAllowedHref(
|
|
176
|
+
input,
|
|
177
|
+
{ baseUrl, allowedOrigins, allowedPathPrefixes = ["/"], allowedProtocols = ["https:"] },
|
|
178
|
+
) {
|
|
179
|
+
try {
|
|
180
|
+
const url = new URL(input, baseUrl);
|
|
181
|
+
if (!allowedProtocols.includes(url.protocol)) return null;
|
|
182
|
+
if (!allowedOrigins.includes(url.origin)) return null;
|
|
183
|
+
if (!allowedPathPrefixes.some((prefix) => matchesPathPrefix(url.pathname, prefix))) return null;
|
|
184
|
+
return url.href;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const href = normalizeAllowedHref(userInput, {
|
|
191
|
+
baseUrl: window.location.origin,
|
|
192
|
+
allowedOrigins: [window.location.origin, "https://docs.example.com"],
|
|
193
|
+
allowedPathPrefixes: ["/docs", "/account"],
|
|
194
|
+
allowedProtocols: ["https:"],
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For links that open in a new browsing context, pair normalized URLs with
|
|
199
|
+
`rel="noopener noreferrer"` or `window.open(..., "noopener,noreferrer")` so the
|
|
200
|
+
new page cannot control the opener.
|
|
201
|
+
|
|
202
|
+
## Content-Type Headers
|
|
203
|
+
|
|
204
|
+
Always set appropriate Content-Type headers:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
// Express.js
|
|
208
|
+
res.setHeader("Content-Type", "application/json");
|
|
209
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
|