cyberaudit-skill 3.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/README.md +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +241 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +126 -0
- package/package.json +58 -0
- package/skills/cyberaudit/AGENT-BOOT.md +122 -0
- package/skills/cyberaudit/COMMANDS.md +1125 -0
- package/skills/cyberaudit/INSTALL.md +311 -0
- package/skills/cyberaudit/MASTER.md +194 -0
- package/skills/cyberaudit/README.md +154 -0
- package/skills/cyberaudit/USAGE-GUIDE.md +107 -0
- package/skills/cyberaudit/mobile/MOBILE-CHECKLIST.md +245 -0
- package/skills/cyberaudit/mobile/MOBILE-PHILOSOPHY.md +352 -0
- package/skills/cyberaudit/mobile/MOBILE-REMEDIATION-LIBRARY.md +468 -0
- package/skills/cyberaudit/mobile/MOBILE-THREAT-MODELS.md +279 -0
- package/skills/cyberaudit/mobile/frameworks/EXPO.md +247 -0
- package/skills/cyberaudit/mobile/frameworks/FLUTTER.md +338 -0
- package/skills/cyberaudit/mobile/frameworks/IONIC.md +248 -0
- package/skills/cyberaudit/mobile/frameworks/REACT-NATIVE.md +479 -0
- package/skills/cyberaudit/mobile/vulnerabilities/AUTH-MOBILE.md +160 -0
- package/skills/cyberaudit/mobile/vulnerabilities/BINARY-ANALYSIS.md +193 -0
- package/skills/cyberaudit/mobile/vulnerabilities/CRYPTO-MOBILE.md +192 -0
- package/skills/cyberaudit/mobile/vulnerabilities/IPC-DEEPLINKS.md +231 -0
- package/skills/cyberaudit/mobile/vulnerabilities/NETWORK-MOBILE.md +165 -0
- package/skills/cyberaudit/mobile/vulnerabilities/PERMISSIONS.md +180 -0
- package/skills/cyberaudit/mobile/vulnerabilities/RUNTIME-MOBILE.md +214 -0
- package/skills/cyberaudit/mobile/vulnerabilities/STORAGE.md +197 -0
- package/skills/cyberaudit/reports/EXECUTIVE-SUMMARY-TEMPLATE.md +188 -0
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-MOBILE.md +410 -0
- package/skills/cyberaudit/reports/REPORT-TEMPLATE-WEB.md +222 -0
- package/skills/cyberaudit/shared/COMPLIANCE.md +307 -0
- package/skills/cyberaudit/shared/CVSS-GUIDE.md +175 -0
- package/skills/cyberaudit/shared/OWASP-MAPPER.md +130 -0
- package/skills/cyberaudit/shared/SEVERITY-SCORING.md +102 -0
- package/skills/cyberaudit/shared/THREAT-MODELING.md +112 -0
- package/skills/cyberaudit/web/WEB-CHECKLIST.md +222 -0
- package/skills/cyberaudit/web/WEB-PHILOSOPHY.md +308 -0
- package/skills/cyberaudit/web/WEB-REMEDIATION-LIBRARY.md +665 -0
- package/skills/cyberaudit/web/WEB-THREAT-MODELS.md +460 -0
- package/skills/cyberaudit/web/frameworks/ANGULAR.md +169 -0
- package/skills/cyberaudit/web/frameworks/EXPRESS.md +185 -0
- package/skills/cyberaudit/web/frameworks/LARAVEL.md +371 -0
- package/skills/cyberaudit/web/frameworks/NESTJS.md +337 -0
- package/skills/cyberaudit/web/frameworks/NEXTJS.md +352 -0
- package/skills/cyberaudit/web/frameworks/REACT.md +346 -0
- package/skills/cyberaudit/web/frameworks/VUE.md +205 -0
- package/skills/cyberaudit/web/vulnerabilities/AUTH-AUTHZ.md +117 -0
- package/skills/cyberaudit/web/vulnerabilities/BUSINESS-LOGIC.md +603 -0
- package/skills/cyberaudit/web/vulnerabilities/CORS.md +117 -0
- package/skills/cyberaudit/web/vulnerabilities/CSRF.md +131 -0
- package/skills/cyberaudit/web/vulnerabilities/DESERIALIZATION.md +96 -0
- package/skills/cyberaudit/web/vulnerabilities/HEADERS.md +105 -0
- package/skills/cyberaudit/web/vulnerabilities/IDOR-BOLA.md +153 -0
- package/skills/cyberaudit/web/vulnerabilities/INJECTION.md +165 -0
- package/skills/cyberaudit/web/vulnerabilities/SECRETS.md +119 -0
- package/skills/cyberaudit/web/vulnerabilities/SSRF.md +124 -0
- package/skills/cyberaudit/web/vulnerabilities/SUPPLY-CHAIN.md +142 -0
- package/skills/cyberaudit/web/vulnerabilities/XSS.md +133 -0
- package/skills/cyberaudit/web/vulnerabilities/XXE.md +94 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# ⚛️ REACT — SECURITY AUDIT GUIDE
|
|
2
|
+
# Comprehensive audit guide for React applications
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
REACT THREAT MODEL
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
FUNDAMENTAL REMINDER:
|
|
9
|
+
React is a FRONTEND framework.
|
|
10
|
+
Backend security is still essential.
|
|
11
|
+
React cannot secure data server-side.
|
|
12
|
+
A React audit = audit of the client-side surface.
|
|
13
|
+
|
|
14
|
+
REACT ATTACK SURFACE
|
|
15
|
+
────────────────────────
|
|
16
|
+
|
|
17
|
+
LAYER 1 — JSX RENDERING
|
|
18
|
+
Risks: XSS via dangerouslySetInnerHTML, innerHTML
|
|
19
|
+
|
|
20
|
+
LAYER 2 — STATE MANAGEMENT
|
|
21
|
+
Risks: Sensitive data in Redux/Context exposed
|
|
22
|
+
State logs in production
|
|
23
|
+
|
|
24
|
+
LAYER 3 — API COMMUNICATION
|
|
25
|
+
Risks: Tokens in localStorage, secrets in code,
|
|
26
|
+
Client-side CORS (false security)
|
|
27
|
+
|
|
28
|
+
LAYER 4 — CLIENT ROUTING
|
|
29
|
+
Risks: Bypassable route guards, unvalidated redirect
|
|
30
|
+
|
|
31
|
+
LAYER 5 — NPM DEPENDENCIES
|
|
32
|
+
Risks: CVEs, supply chain, malicious scripts
|
|
33
|
+
|
|
34
|
+
LAYER 6 — BUILD AND DEPLOYMENT
|
|
35
|
+
Risks: Exposed source maps, env vars in bundle
|
|
36
|
+
|
|
37
|
+
═══════════════════════════════════════════════════════════════
|
|
38
|
+
VULNERABLE REACT PATTERNS
|
|
39
|
+
═══════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
XSS VIA DANGEROUS RENDERING
|
|
43
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
|
|
45
|
+
CRITICAL PATTERN — Unsanitized dangerouslySetInnerHTML
|
|
46
|
+
DETECT:
|
|
47
|
+
<div dangerouslySetInnerHTML={{ __html: userContent }} />
|
|
48
|
+
<div dangerouslySetInnerHTML={{ __html: comment.body }} />
|
|
49
|
+
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
|
50
|
+
|
|
51
|
+
FIX:
|
|
52
|
+
import DOMPurify from 'dompurify'
|
|
53
|
+
|
|
54
|
+
const SafeHTML = ({ content }: { content: string }) => {
|
|
55
|
+
const sanitized = DOMPurify.sanitize(content, {
|
|
56
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
|
|
57
|
+
ALLOWED_ATTR: [], // No href, onclick, etc. attributes
|
|
58
|
+
FORBID_TAGS: ['script', 'style', 'iframe', 'object'],
|
|
59
|
+
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Even better: use react-markdown for markdown content
|
|
66
|
+
import ReactMarkdown from 'react-markdown'
|
|
67
|
+
import remarkGfm from 'remark-gfm'
|
|
68
|
+
|
|
69
|
+
const SafeMarkdown = ({ content }: { content: string }) => (
|
|
70
|
+
<ReactMarkdown
|
|
71
|
+
remarkPlugins={[remarkGfm]}
|
|
72
|
+
components={{
|
|
73
|
+
// Disable dangerous elements
|
|
74
|
+
script: () => null,
|
|
75
|
+
iframe: () => null,
|
|
76
|
+
// Secure links
|
|
77
|
+
a: ({ href, children }) => {
|
|
78
|
+
const isExternal = href?.startsWith('http')
|
|
79
|
+
return (
|
|
80
|
+
<a
|
|
81
|
+
href={href}
|
|
82
|
+
target={isExternal ? '_blank' : undefined}
|
|
83
|
+
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</a>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{content}
|
|
92
|
+
</ReactMarkdown>
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
CRITICAL PATTERN — eval() and new Function()
|
|
96
|
+
DETECT:
|
|
97
|
+
eval(userInput)
|
|
98
|
+
eval(`return ${expression}`)
|
|
99
|
+
new Function('x', userCode)()
|
|
100
|
+
setTimeout(userString, 100) // setTimeout with string!
|
|
101
|
+
setInterval(userString, 100) // setInterval with string!
|
|
102
|
+
|
|
103
|
+
FIX:
|
|
104
|
+
// Never evaluate user code
|
|
105
|
+
// Replace with static mappings
|
|
106
|
+
|
|
107
|
+
// ❌ Instead of:
|
|
108
|
+
const result = eval(userExpression)
|
|
109
|
+
|
|
110
|
+
// ✅ Use a secure evaluation library
|
|
111
|
+
// or a functional approach:
|
|
112
|
+
const SAFE_OPERATIONS: Record<string, (a: number, b: number) => number> = {
|
|
113
|
+
add: (a, b) => a + b,
|
|
114
|
+
subtract: (a, b) => a - b,
|
|
115
|
+
multiply: (a, b) => a * b,
|
|
116
|
+
divide: (a, b) => b !== 0 ? a / b : 0,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = SAFE_OPERATIONS[operation]?.(numA, numB) ?? 0
|
|
120
|
+
|
|
121
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
122
|
+
CLIENT-SIDE TOKEN STORAGE
|
|
123
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
124
|
+
|
|
125
|
+
HIGH PATTERN — Tokens in localStorage
|
|
126
|
+
DETECT:
|
|
127
|
+
localStorage.setItem('token', accessToken)
|
|
128
|
+
localStorage.setItem('auth', JSON.stringify(authData))
|
|
129
|
+
localStorage.getItem('token')
|
|
130
|
+
sessionStorage.setItem('jwt', token)
|
|
131
|
+
|
|
132
|
+
UNDERSTAND:
|
|
133
|
+
localStorage = accessible via JavaScript
|
|
134
|
+
XSS → token theft → session hijacking
|
|
135
|
+
No automatic expiration
|
|
136
|
+
Shared across all tabs
|
|
137
|
+
|
|
138
|
+
FIX:
|
|
139
|
+
// OPTION A — HttpOnly Cookies (recommended)
|
|
140
|
+
// Token is managed server-side, not accessible via JS
|
|
141
|
+
// Login via API that sets the cookie
|
|
142
|
+
|
|
143
|
+
// React side: no token management at all
|
|
144
|
+
const login = async (credentials) => {
|
|
145
|
+
await fetch('/api/auth/login', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
credentials: 'include', // Sends and receives cookies
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(credentials),
|
|
150
|
+
})
|
|
151
|
+
// HttpOnly cookie is automatically set by the server
|
|
152
|
+
// React does not have access to the token
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// OPTION B — Memory only (if cookies impossible)
|
|
156
|
+
// Token in React memory, never in storage
|
|
157
|
+
// Lost on reload → use a refresh token in cookie
|
|
158
|
+
|
|
159
|
+
let inMemoryToken: string | null = null
|
|
160
|
+
|
|
161
|
+
export const authService = {
|
|
162
|
+
setToken: (token: string) => { inMemoryToken = token },
|
|
163
|
+
getToken: () => inMemoryToken,
|
|
164
|
+
clearToken: () => { inMemoryToken = null },
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
168
|
+
SECRETS IN THE BUNDLE
|
|
169
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
170
|
+
|
|
171
|
+
CRITICAL PATTERN — Env vars in the bundle
|
|
172
|
+
DETECT:
|
|
173
|
+
// Everything starting with REACT_APP_ is in the bundle
|
|
174
|
+
const apiKey = process.env.REACT_APP_SECRET_KEY
|
|
175
|
+
const dbUrl = process.env.REACT_APP_DATABASE_URL
|
|
176
|
+
|
|
177
|
+
UNDERSTAND:
|
|
178
|
+
REACT_APP_* = injected into the JavaScript bundle
|
|
179
|
+
anyone can see these values via DevTools
|
|
180
|
+
or by downloading the .js file of the app
|
|
181
|
+
|
|
182
|
+
FIX:
|
|
183
|
+
// Client-side variables (OK if truly public):
|
|
184
|
+
REACT_APP_API_URL=https://api.yourapp.com ✅
|
|
185
|
+
REACT_APP_ANALYTICS_ID=G-XXXXXXXXXX ✅
|
|
186
|
+
REACT_APP_STRIPE_PUBLIC_KEY=pk_live_... ✅
|
|
187
|
+
|
|
188
|
+
// Secrets: NEVER in REACT_APP_
|
|
189
|
+
// They must be on the server
|
|
190
|
+
DATABASE_URL=... ← Server only
|
|
191
|
+
STRIPE_SECRET=... ← Server only
|
|
192
|
+
JWT_SECRET=... ← Server only
|
|
193
|
+
|
|
194
|
+
// If you need a server value on the client:
|
|
195
|
+
// Create an API endpoint that returns the value
|
|
196
|
+
// after auth verification
|
|
197
|
+
|
|
198
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
|
+
CLIENT ROUTE GUARDS
|
|
200
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
201
|
+
|
|
202
|
+
HIGH PATTERN — Client-side only protection
|
|
203
|
+
DETECT:
|
|
204
|
+
// Purely client-side protection = bypassable
|
|
205
|
+
const PrivateRoute = ({ children }) => {
|
|
206
|
+
const token = localStorage.getItem('token')
|
|
207
|
+
return token ? children : <Navigate to="/login" />
|
|
208
|
+
}
|
|
209
|
+
// An attacker can set token in localStorage
|
|
210
|
+
// and access all "protected" components
|
|
211
|
+
|
|
212
|
+
FIX:
|
|
213
|
+
// React routes = UX only
|
|
214
|
+
// Real protection = server
|
|
215
|
+
|
|
216
|
+
// React side: verify via the server
|
|
217
|
+
const PrivateRoute = ({ children }) => {
|
|
218
|
+
const [isAuth, setIsAuth] = useState<boolean | null>(null)
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
// Server-side verification, not localStorage
|
|
222
|
+
fetch('/api/auth/me', { credentials: 'include' })
|
|
223
|
+
.then(res => setIsAuth(res.ok))
|
|
224
|
+
.catch(() => setIsAuth(false))
|
|
225
|
+
}, [])
|
|
226
|
+
|
|
227
|
+
if (isAuth === null) return <LoadingSpinner />
|
|
228
|
+
if (!isAuth) return <Navigate to="/login" replace />
|
|
229
|
+
return children
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// AND server-side: each API route verifies auth
|
|
233
|
+
// independently of React routing
|
|
234
|
+
|
|
235
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
236
|
+
REACT ROUTER OPEN REDIRECT
|
|
237
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
238
|
+
|
|
239
|
+
MEDIUM PATTERN — Redirect with unvalidated URL parameter
|
|
240
|
+
DETECT:
|
|
241
|
+
const redirectTo = new URLSearchParams(location.search).get('next')
|
|
242
|
+
navigate(redirectTo || '/dashboard')
|
|
243
|
+
|
|
244
|
+
const { returnUrl } = queryParams
|
|
245
|
+
window.location.href = returnUrl // CRITICAL: domain exit possible
|
|
246
|
+
|
|
247
|
+
FIX:
|
|
248
|
+
function getSafeRedirect(redirectParam: string | null): string {
|
|
249
|
+
if (!redirectParam) return '/dashboard'
|
|
250
|
+
|
|
251
|
+
// Only allow relative paths from our app
|
|
252
|
+
// A relative path starts with / but not //
|
|
253
|
+
if (
|
|
254
|
+
redirectParam.startsWith('/') &&
|
|
255
|
+
!redirectParam.startsWith('//') &&
|
|
256
|
+
!redirectParam.startsWith('/\\')
|
|
257
|
+
) {
|
|
258
|
+
// Verify the path is in our whitelist
|
|
259
|
+
const allowedPaths = ['/dashboard', '/profile',
|
|
260
|
+
'/orders', '/settings']
|
|
261
|
+
const isAllowed = allowedPaths.some(
|
|
262
|
+
path => redirectParam.startsWith(path)
|
|
263
|
+
)
|
|
264
|
+
return isAllowed ? redirectParam : '/dashboard'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return '/dashboard'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Usage
|
|
271
|
+
const redirect = getSafeRedirect(searchParams.get('next'))
|
|
272
|
+
navigate(redirect)
|
|
273
|
+
|
|
274
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
275
|
+
SOURCE MAPS IN PRODUCTION
|
|
276
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
277
|
+
|
|
278
|
+
MEDIUM PATTERN — Exposed source maps
|
|
279
|
+
DETECT:
|
|
280
|
+
// In package.json or .env:
|
|
281
|
+
GENERATE_SOURCEMAP=true (CRA default in prod)
|
|
282
|
+
// .js.map files publicly accessible
|
|
283
|
+
|
|
284
|
+
IMPACT:
|
|
285
|
+
→ Reconstruction of original source code
|
|
286
|
+
→ Business logic exposed
|
|
287
|
+
→ Internal file paths visible
|
|
288
|
+
→ Original comments and variable names readable
|
|
289
|
+
|
|
290
|
+
FIX:
|
|
291
|
+
// Create React App
|
|
292
|
+
GENERATE_SOURCEMAP=false (in .env.production)
|
|
293
|
+
|
|
294
|
+
// Vite — vite.config.ts
|
|
295
|
+
export default defineConfig({
|
|
296
|
+
build: {
|
|
297
|
+
sourcemap: false, // In production
|
|
298
|
+
// OR send source maps to Sentry only:
|
|
299
|
+
sourcemap: true, // Generate
|
|
300
|
+
// And configure Sentry to upload them then remove
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Webpack
|
|
305
|
+
module.exports = {
|
|
306
|
+
devtool: process.env.NODE_ENV === 'production'
|
|
307
|
+
? false // No source maps in prod
|
|
308
|
+
: 'source-map', // Source maps in dev
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
═══════════════════════════════════════════════════════════════
|
|
312
|
+
COMPLETE REACT CHECKLIST
|
|
313
|
+
═══════════════════════════════════════════════════════════════
|
|
314
|
+
|
|
315
|
+
XSS AND RENDERING
|
|
316
|
+
□ dangerouslySetInnerHTML with DOMPurify if used?
|
|
317
|
+
□ eval() / new Function() absent?
|
|
318
|
+
□ Direct innerHTML absent?
|
|
319
|
+
□ External links with rel="noopener noreferrer"?
|
|
320
|
+
□ CSP configured on the deployment server?
|
|
321
|
+
|
|
322
|
+
TOKEN MANAGEMENT
|
|
323
|
+
□ Tokens in HttpOnly cookies (not localStorage)?
|
|
324
|
+
□ No token in sessionStorage if XSS risky?
|
|
325
|
+
□ Memory-only as acceptable alternative?
|
|
326
|
+
|
|
327
|
+
VARIABLES AND SECRETS
|
|
328
|
+
□ No secrets in REACT_APP_?
|
|
329
|
+
□ GENERATE_SOURCEMAP=false in production?
|
|
330
|
+
□ .env.production in .gitignore?
|
|
331
|
+
|
|
332
|
+
ROUTING
|
|
333
|
+
□ Systematic server-side protection (not just React)?
|
|
334
|
+
□ Redirects validated with whitelist?
|
|
335
|
+
□ URL parameters sanitized before use?
|
|
336
|
+
|
|
337
|
+
DEPENDENCIES
|
|
338
|
+
□ npm audit clean?
|
|
339
|
+
□ Dependencies up to date?
|
|
340
|
+
□ Lock file committed?
|
|
341
|
+
□ Unused dependencies removed?
|
|
342
|
+
|
|
343
|
+
API COMMUNICATION
|
|
344
|
+
□ credentials: 'include' if HttpOnly cookies?
|
|
345
|
+
□ HTTPS only?
|
|
346
|
+
□ Error handling does not reveal internal details?
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# 💚 VUE.JS — SECURITY AUDIT GUIDE
|
|
2
|
+
|
|
3
|
+
═══════════════════════════════════════════════════════════════
|
|
4
|
+
VUE.JS THREAT MODEL
|
|
5
|
+
═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
VUE AUTO-ESCAPE:
|
|
8
|
+
Vue automatically escapes data in templates.
|
|
9
|
+
{{ userInput }} is safe by default.
|
|
10
|
+
XSS risks come from exceptions to this rule.
|
|
11
|
+
|
|
12
|
+
VUE ATTACK SURFACE
|
|
13
|
+
──────────────────────
|
|
14
|
+
LAYER 1 — TEMPLATES (v-html, :href)
|
|
15
|
+
Risks: XSS via v-html, javascript: in href
|
|
16
|
+
|
|
17
|
+
LAYER 2 — DYNAMIC COMPONENTS
|
|
18
|
+
Risks: <component :is="userInput"> → XSS / injection
|
|
19
|
+
|
|
20
|
+
LAYER 3 — SSR (Nuxt.js)
|
|
21
|
+
Risks: Hydration mismatch, server-side injection
|
|
22
|
+
|
|
23
|
+
LAYER 4 — PINIA / VUEX
|
|
24
|
+
Risks: State exposure, persisted sensitive data
|
|
25
|
+
|
|
26
|
+
LAYER 5 — VUE ROUTER
|
|
27
|
+
Risks: Bypassable route guards, navigation guards
|
|
28
|
+
|
|
29
|
+
═══════════════════════════════════════════════════════════════
|
|
30
|
+
VULNERABLE VUE PATTERNS
|
|
31
|
+
═══════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
XSS VIA v-html
|
|
35
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
|
|
37
|
+
HIGH PATTERN — v-html with user content
|
|
38
|
+
DETECT:
|
|
39
|
+
<div v-html="userComment"></div>
|
|
40
|
+
<div v-html="product.description"></div>
|
|
41
|
+
<span v-html="formattedContent"></span>
|
|
42
|
+
|
|
43
|
+
FIX:
|
|
44
|
+
// Install DOMPurify
|
|
45
|
+
// npm install dompurify @types/dompurify
|
|
46
|
+
|
|
47
|
+
// Custom Vue directive
|
|
48
|
+
// directives/safe-html.ts
|
|
49
|
+
import DOMPurify from 'dompurify'
|
|
50
|
+
import type { Directive } from 'vue'
|
|
51
|
+
|
|
52
|
+
export const safeHtml: Directive = {
|
|
53
|
+
mounted(el, binding) {
|
|
54
|
+
el.innerHTML = DOMPurify.sanitize(binding.value, {
|
|
55
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
|
|
56
|
+
ALLOWED_ATTR: [],
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
updated(el, binding) {
|
|
60
|
+
el.innerHTML = DOMPurify.sanitize(binding.value, {
|
|
61
|
+
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
|
|
62
|
+
ALLOWED_ATTR: [],
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Global registration
|
|
68
|
+
// main.ts
|
|
69
|
+
app.directive('safe-html', safeHtml)
|
|
70
|
+
|
|
71
|
+
// Usage in templates
|
|
72
|
+
<div v-safe-html="userComment"></div>
|
|
73
|
+
|
|
74
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
75
|
+
UNCONTROLLED DYNAMIC COMPONENTS
|
|
76
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
|
+
|
|
78
|
+
HIGH PATTERN — :is with user value
|
|
79
|
+
DETECT:
|
|
80
|
+
<component :is="userInput" />
|
|
81
|
+
<component :is="route.params.componentName" />
|
|
82
|
+
|
|
83
|
+
FIX:
|
|
84
|
+
// Whitelist of allowed components
|
|
85
|
+
import CardComponent from './Card.vue'
|
|
86
|
+
import ListComponent from './List.vue'
|
|
87
|
+
import TableComponent from './Table.vue'
|
|
88
|
+
|
|
89
|
+
const ALLOWED_COMPONENTS = {
|
|
90
|
+
card: CardComponent,
|
|
91
|
+
list: ListComponent,
|
|
92
|
+
table: TableComponent,
|
|
93
|
+
} as const
|
|
94
|
+
|
|
95
|
+
const safeComponent = computed(() => {
|
|
96
|
+
return ALLOWED_COMPONENTS[userInput] || CardComponent
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
<component :is="safeComponent" />
|
|
100
|
+
|
|
101
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
102
|
+
VUE ROUTER — NAVIGATION GUARDS
|
|
103
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
|
|
105
|
+
HIGH PATTERN — Guard based on local state alone
|
|
106
|
+
DETECT:
|
|
107
|
+
router.beforeEach((to, from, next) => {
|
|
108
|
+
const authStore = useAuthStore()
|
|
109
|
+
// Based solely on Pinia state
|
|
110
|
+
// No server verification
|
|
111
|
+
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
|
112
|
+
next('/login')
|
|
113
|
+
} else {
|
|
114
|
+
next()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
FIX:
|
|
119
|
+
router.beforeEach(async (to, from, next) => {
|
|
120
|
+
if (!to.meta.requiresAuth) return next()
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Server-side verification
|
|
124
|
+
const response = await fetch('/api/auth/verify', {
|
|
125
|
+
credentials: 'include'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
return next({
|
|
130
|
+
path: '/login',
|
|
131
|
+
query: { redirect: to.fullPath }
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { user } = await response.json()
|
|
136
|
+
|
|
137
|
+
// Role check if necessary
|
|
138
|
+
if (to.meta.requiredRole && user.role !== to.meta.requiredRole) {
|
|
139
|
+
return next('/unauthorized')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
next()
|
|
143
|
+
} catch {
|
|
144
|
+
next('/login')
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
149
|
+
NUXT.JS — SSR SPECIFIC
|
|
150
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
151
|
+
|
|
152
|
+
HIGH PATTERN — useRoute() with unvalidated params in SSR
|
|
153
|
+
DETECT:
|
|
154
|
+
// pages/user/[id].vue
|
|
155
|
+
const route = useRoute()
|
|
156
|
+
const { data } = await useFetch(`/api/user/${route.params.id}`)
|
|
157
|
+
// route.params.id unvalidated = possible SSRF or injection
|
|
158
|
+
|
|
159
|
+
FIX:
|
|
160
|
+
const route = useRoute()
|
|
161
|
+
const userId = route.params.id
|
|
162
|
+
|
|
163
|
+
// Validate the ID format
|
|
164
|
+
if (!/^[0-9a-f-]{36}$/.test(userId as string)) {
|
|
165
|
+
throw createError({ statusCode: 400, message: 'Invalid ID' })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { data } = await useFetch(`/api/user/${userId}`, {
|
|
169
|
+
// Security headers passed if needed
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
MEDIUM PATTERN — Secrets in nuxt.config.ts on the public side
|
|
173
|
+
DETECT:
|
|
174
|
+
// nuxt.config.ts
|
|
175
|
+
runtimeConfig: {
|
|
176
|
+
public: {
|
|
177
|
+
apiSecret: process.env.API_SECRET, // EXPOSED TO CLIENT!
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
FIX:
|
|
182
|
+
runtimeConfig: {
|
|
183
|
+
// Server-side only (private)
|
|
184
|
+
apiSecret: process.env.API_SECRET,
|
|
185
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
186
|
+
|
|
187
|
+
// Client-side (public) — only if truly public
|
|
188
|
+
public: {
|
|
189
|
+
apiUrl: process.env.NUXT_PUBLIC_API_URL,
|
|
190
|
+
appName: 'MyApp',
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
═══════════════════════════════════════════════════════════════
|
|
195
|
+
COMPLETE VUE.JS CHECKLIST
|
|
196
|
+
═══════════════════════════════════════════════════════════════
|
|
197
|
+
|
|
198
|
+
□ v-html with sanitization (DOMPurify)?
|
|
199
|
+
□ Dynamic components with whitelist?
|
|
200
|
+
□ Navigation guards with server verification?
|
|
201
|
+
□ Tokens in HttpOnly cookies (not localStorage)?
|
|
202
|
+
□ Nuxt: secrets in private runtimeConfig?
|
|
203
|
+
□ Nuxt: useRoute() params validated server-side?
|
|
204
|
+
□ Source maps disabled in production?
|
|
205
|
+
□ npm audit clean?
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# 🔐 AUTH & AUTHZ — VULNERABILITY GUIDE
|
|
2
|
+
|
|
3
|
+
═══════════════════════════════════════════════════════════════
|
|
4
|
+
AUTHENTICATION VS AUTHORIZATION
|
|
5
|
+
═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
AUTHENTICATION = "Who are you?"
|
|
8
|
+
Verify a user's identity.
|
|
9
|
+
Mechanisms : Password, MFA, Biometrics, SSO, OAuth
|
|
10
|
+
|
|
11
|
+
AUTHORIZATION = "Do you have permission?"
|
|
12
|
+
Verify if the authenticated user can perform this action.
|
|
13
|
+
Mechanisms : RBAC, ABAC, ACL, Policy-based
|
|
14
|
+
|
|
15
|
+
BOTH CAN BE COMPROMISED INDEPENDENTLY.
|
|
16
|
+
Strong Auth + Weak Authz = IDOR, privilege escalation
|
|
17
|
+
Weak Auth + Strong Authz = account takeover then escalation
|
|
18
|
+
|
|
19
|
+
OWASP : A07:2021 (Auth) + A01:2021 (Authz)
|
|
20
|
+
|
|
21
|
+
═══════════════════════════════════════════════════════════════
|
|
22
|
+
AUTHENTICATION FLAWS
|
|
23
|
+
═══════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
BRUTE FORCE
|
|
26
|
+
Detect : No rate limiting on /login
|
|
27
|
+
Impact : Account compromise with weak passwords
|
|
28
|
+
Fix : Rate limiting + lockout + CAPTCHA + increasing delay
|
|
29
|
+
|
|
30
|
+
CREDENTIAL STUFFING
|
|
31
|
+
Detect : Same accounts as on other services
|
|
32
|
+
Impact : Mass compromise if passwords reused
|
|
33
|
+
Fix : Mandatory MFA + credential stuffing detection
|
|
34
|
+
(HaveIBeenPwned API, delays, device fingerprinting)
|
|
35
|
+
|
|
36
|
+
PASSWORD RESET HIJACKING
|
|
37
|
+
Detect :
|
|
38
|
+
- Predictable token (timestamp, MD5 hash of email)
|
|
39
|
+
- No token expiration
|
|
40
|
+
- Reusable token
|
|
41
|
+
- Message revealing if email exists
|
|
42
|
+
Impact : Full account takeover
|
|
43
|
+
Fix :
|
|
44
|
+
const resetToken = crypto.randomBytes(32).toString('hex')
|
|
45
|
+
const hashedToken = crypto
|
|
46
|
+
.createHash('sha256')
|
|
47
|
+
.update(resetToken)
|
|
48
|
+
.digest('hex')
|
|
49
|
+
|
|
50
|
+
await db.passwordReset.create({
|
|
51
|
+
data: {
|
|
52
|
+
userId: user.id,
|
|
53
|
+
token: hashedToken, // Store hash, send raw
|
|
54
|
+
expiresAt: new Date(Date.now() + 15 * 60 * 1000), // 15min
|
|
55
|
+
used: false,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
SESSION FIXATION
|
|
60
|
+
Detect : Session ID not regenerated after login
|
|
61
|
+
Impact : Attacker forces a known session ID, then user logs in
|
|
62
|
+
Fix : req.session.regenerate() after every authentication
|
|
63
|
+
|
|
64
|
+
JWT VULNERABILITIES
|
|
65
|
+
Detect :
|
|
66
|
+
- "none" algorithm accepted
|
|
67
|
+
- jwt.decode() instead of jwt.verify()
|
|
68
|
+
- Short or predictable secret
|
|
69
|
+
- No expiration
|
|
70
|
+
- Algorithm confusion (RS256 → HS256 with public key)
|
|
71
|
+
Impact : Forged token = identity theft
|
|
72
|
+
Fix : See REM-003 in remediation library
|
|
73
|
+
|
|
74
|
+
═══════════════════════════════════════════════════════════════
|
|
75
|
+
AUTHORIZATION FLAWS
|
|
76
|
+
═══════════════════════════════════════════════════════════════
|
|
77
|
+
|
|
78
|
+
IDOR (Insecure Direct Object Reference)
|
|
79
|
+
See : web/vulnerabilities/IDOR-BOLA.md
|
|
80
|
+
|
|
81
|
+
BFLA (Broken Function Level Authorization)
|
|
82
|
+
Detect :
|
|
83
|
+
- Admin endpoints accessible without role check
|
|
84
|
+
- Destructive actions without verification
|
|
85
|
+
- Unrestricted HTTP methods (DELETE without auth)
|
|
86
|
+
Impact : Access to admin functions by a normal user
|
|
87
|
+
Fix :
|
|
88
|
+
// Role verification on EVERY admin endpoint
|
|
89
|
+
@Get('admin/users')
|
|
90
|
+
@Roles('ADMIN', 'SUPER_ADMIN')
|
|
91
|
+
async getUsers() { ... }
|
|
92
|
+
|
|
93
|
+
// Do not rely on the URL starting with /admin
|
|
94
|
+
// Verify role explicitly even under /admin
|
|
95
|
+
|
|
96
|
+
PRIVILEGE ESCALATION
|
|
97
|
+
Detect :
|
|
98
|
+
- "role" field accepted in user updates
|
|
99
|
+
- is_admin parameter in body
|
|
100
|
+
- JWT with modifiable role on client side
|
|
101
|
+
Impact : Normal user becomes admin
|
|
102
|
+
Fix :
|
|
103
|
+
// Never update role from user input
|
|
104
|
+
// Except through an explicitly protected admin endpoint
|
|
105
|
+
|
|
106
|
+
// ❌ DANGEROUS :
|
|
107
|
+
await user.update(req.body) // req.body may contain role: "admin"
|
|
108
|
+
|
|
109
|
+
// ✅ SECURE :
|
|
110
|
+
await user.update({
|
|
111
|
+
name: req.body.name,
|
|
112
|
+
email: req.body.email,
|
|
113
|
+
// role → never from req.body
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
MASS ASSIGNMENT
|
|
117
|
+
See : web/frameworks/LARAVEL.md (Pattern 5 and 6)
|