@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.
@@ -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