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,479 @@
|
|
|
1
|
+
# ⚛️ REACT NATIVE — SECURITY AUDIT GUIDE
|
|
2
|
+
# Comprehensive audit guide for React Native applications
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
REACT NATIVE — SECURITY ARCHITECTURE
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
UNDERSTANDING THE ARCHITECTURE TO AUDIT CORRECTLY
|
|
9
|
+
─────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
[JavaScript Thread] ←→ [Bridge / JSI] ←→ [Native Thread]
|
|
12
|
+
↑ ↑
|
|
13
|
+
React Native Code Native modules
|
|
14
|
+
Business logic System APIs
|
|
15
|
+
App state Storage, Camera...
|
|
16
|
+
|
|
17
|
+
SECURITY IMPLICATIONS:
|
|
18
|
+
→ The JS bundle is packaged in the app = extractable
|
|
19
|
+
→ The Bridge = attack surface between JS and native
|
|
20
|
+
→ Expo managed = less control over native modules
|
|
21
|
+
→ Bare workflow = more control, more responsibility
|
|
22
|
+
|
|
23
|
+
═══════════════════════════════════════════════════════════════
|
|
24
|
+
VULNERABLE REACT NATIVE PATTERNS
|
|
25
|
+
═══════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
|
+
UNSECURE STORAGE
|
|
29
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
30
|
+
|
|
31
|
+
CRITICAL PATTERN 1 — AsyncStorage with sensitive data
|
|
32
|
+
DETECT:
|
|
33
|
+
AsyncStorage.setItem('token', accessToken)
|
|
34
|
+
AsyncStorage.setItem('user_data', JSON.stringify(user))
|
|
35
|
+
AsyncStorage.setItem('password', password)
|
|
36
|
+
AsyncStorage.setItem('credit_card', cardNumber)
|
|
37
|
+
AsyncStorage.setItem('session', sessionId)
|
|
38
|
+
|
|
39
|
+
// Also via wrappers:
|
|
40
|
+
storage.set('auth', token) // if storage = AsyncStorage
|
|
41
|
+
cache.store('user', data) // check implementation
|
|
42
|
+
|
|
43
|
+
UNDERSTAND:
|
|
44
|
+
AsyncStorage = unencrypted files on the system
|
|
45
|
+
Android: /data/data/com.yourapp/databases/
|
|
46
|
+
iOS: NSLibraryDirectory unencrypted
|
|
47
|
+
Readable with root/jailbreak OR unencrypted backup
|
|
48
|
+
|
|
49
|
+
FIX:
|
|
50
|
+
// Installation
|
|
51
|
+
// yarn add react-native-keychain
|
|
52
|
+
|
|
53
|
+
import * as Keychain from 'react-native-keychain'
|
|
54
|
+
|
|
55
|
+
// Store the auth token
|
|
56
|
+
await Keychain.setGenericPassword(
|
|
57
|
+
'auth-token',
|
|
58
|
+
accessToken,
|
|
59
|
+
{
|
|
60
|
+
service: 'com.yourapp.auth',
|
|
61
|
+
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE,
|
|
62
|
+
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
|
63
|
+
// WHEN_UNLOCKED_THIS_DEVICE_ONLY = not in iCloud backups
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// Retrieve the token
|
|
68
|
+
const credentials = await Keychain.getGenericPassword({
|
|
69
|
+
service: 'com.yourapp.auth'
|
|
70
|
+
})
|
|
71
|
+
const token = credentials ? credentials.password : null
|
|
72
|
+
|
|
73
|
+
// For less sensitive data (preferences, config)
|
|
74
|
+
// react-native-encrypted-storage is acceptable
|
|
75
|
+
import EncryptedStorage from 'react-native-encrypted-storage'
|
|
76
|
+
|
|
77
|
+
await EncryptedStorage.setItem(
|
|
78
|
+
'user_preferences',
|
|
79
|
+
JSON.stringify(preferences)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
HIGH PATTERN 2 — MMKV / Redux Persist without encryption
|
|
83
|
+
DETECT:
|
|
84
|
+
import { MMKV } from 'react-native-mmkv'
|
|
85
|
+
const storage = new MMKV() // No encryption!
|
|
86
|
+
storage.set('user.token', token)
|
|
87
|
+
|
|
88
|
+
// Redux Persist with AsyncStorage
|
|
89
|
+
const persistConfig = {
|
|
90
|
+
key: 'root',
|
|
91
|
+
storage: AsyncStorage, // Unencrypted!
|
|
92
|
+
// Whitelist without filtering sensitive data
|
|
93
|
+
whitelist: ['auth', 'user', 'payment']
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
FIX:
|
|
97
|
+
// MMKV with encryption
|
|
98
|
+
import { MMKV } from 'react-native-mmkv'
|
|
99
|
+
|
|
100
|
+
// Encryption key must come from Keychain!
|
|
101
|
+
const encryptionKey = await getEncryptionKeyFromKeychain()
|
|
102
|
+
|
|
103
|
+
const storage = new MMKV({
|
|
104
|
+
id: 'secure-storage',
|
|
105
|
+
encryptionKey: encryptionKey
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Redux Persist: do NOT persist sensitive data
|
|
109
|
+
const persistConfig = {
|
|
110
|
+
key: 'root',
|
|
111
|
+
storage: MMKVStorage, // with encryption
|
|
112
|
+
// Blacklist sensitive data
|
|
113
|
+
blacklist: ['auth', 'payment'],
|
|
114
|
+
// OR whitelist only what is safe
|
|
115
|
+
whitelist: ['ui', 'settings', 'theme']
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Auth tokens → Keychain only
|
|
119
|
+
|
|
120
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
121
|
+
UNSECURE WEBVIEW
|
|
122
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
123
|
+
|
|
124
|
+
CRITICAL PATTERN 3 — WebView with exposed JS bridge
|
|
125
|
+
DETECT:
|
|
126
|
+
<WebView
|
|
127
|
+
source={{ uri: userUrl }} // User-controlled URL!
|
|
128
|
+
javaScriptEnabled={true}
|
|
129
|
+
onMessage={handleMessage} // Exposed bridge
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
// Even worse:
|
|
133
|
+
<WebView
|
|
134
|
+
source={{ uri: `https://yourapp.com?data=${userInput}` }}
|
|
135
|
+
injectedJavaScript={userScript} // CRITICAL
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
FIX:
|
|
139
|
+
<WebView
|
|
140
|
+
// Controlled source
|
|
141
|
+
source={{ uri: 'https://trusted.yourapp.com/content' }}
|
|
142
|
+
|
|
143
|
+
// If dynamic URL, strict whitelist
|
|
144
|
+
onShouldStartLoadWithRequest={(request) => {
|
|
145
|
+
const allowedHosts = ['yourapp.com', 'trusted-partner.com']
|
|
146
|
+
const host = new URL(request.url).hostname
|
|
147
|
+
return allowedHosts.includes(host)
|
|
148
|
+
}}
|
|
149
|
+
|
|
150
|
+
// Messages validated on React Native side
|
|
151
|
+
onMessage={(event) => {
|
|
152
|
+
const data = JSON.parse(event.nativeEvent.data)
|
|
153
|
+
|
|
154
|
+
// Validate message type
|
|
155
|
+
const allowedTypes = ['navigation', 'shareContent', 'payment_result']
|
|
156
|
+
if (!allowedTypes.includes(data.type)) return
|
|
157
|
+
|
|
158
|
+
// Validate data by type
|
|
159
|
+
handleValidatedMessage(data)
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
// Disable JS if not needed
|
|
163
|
+
javaScriptEnabled={false} // If possible
|
|
164
|
+
|
|
165
|
+
// Disable local file access
|
|
166
|
+
allowFileAccess={false}
|
|
167
|
+
allowFileAccessFromFileURLs={false}
|
|
168
|
+
allowUniversalAccessFromFileURLs={false}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
172
|
+
LOGS IN PRODUCTION
|
|
173
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
174
|
+
|
|
175
|
+
MEDIUM PATTERN 4 — console.log with sensitive data
|
|
176
|
+
DETECT:
|
|
177
|
+
console.log('User logged in:', user)
|
|
178
|
+
console.log('Token:', token)
|
|
179
|
+
console.log('API Response:', response.data)
|
|
180
|
+
console.log('Payment data:', paymentInfo)
|
|
181
|
+
console.warn('Auth error:', authError) // contains details?
|
|
182
|
+
|
|
183
|
+
// Often forgotten patterns:
|
|
184
|
+
console.log(JSON.stringify(reduxState)) // Full state!
|
|
185
|
+
console.log('Request:', config) // Headers with auth token!
|
|
186
|
+
|
|
187
|
+
FIX:
|
|
188
|
+
// utils/logger.ts
|
|
189
|
+
const isDev = __DEV__
|
|
190
|
+
|
|
191
|
+
export const logger = {
|
|
192
|
+
// Log only in development
|
|
193
|
+
debug: (...args: any[]) => {
|
|
194
|
+
if (isDev) console.log('[DEBUG]', ...args)
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
info: (...args: any[]) => {
|
|
198
|
+
if (isDev) console.info('[INFO]', ...args)
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
warn: (...args: any[]) => {
|
|
202
|
+
if (isDev) console.warn('[WARN]', ...args)
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Errors can go to crash reporting service
|
|
206
|
+
// but WITHOUT sensitive data
|
|
207
|
+
error: (message: string, error?: Error) => {
|
|
208
|
+
if (isDev) console.error('[ERROR]', message, error)
|
|
209
|
+
|
|
210
|
+
// In production: send only message, not data
|
|
211
|
+
crashReporter.log(message, {
|
|
212
|
+
errorType: error?.name,
|
|
213
|
+
// No full stack trace with user data
|
|
214
|
+
})
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Never log this data, even in dev
|
|
218
|
+
sensitive: () => {
|
|
219
|
+
// Intentional no-op
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
224
|
+
REACT NATIVE DEEP LINKS
|
|
225
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
226
|
+
|
|
227
|
+
HIGH PATTERN 5 — Deep links without validation
|
|
228
|
+
DETECT:
|
|
229
|
+
// Basic handling without validation
|
|
230
|
+
Linking.addEventListener('url', ({ url }) => {
|
|
231
|
+
const route = url.replace('myapp://', '')
|
|
232
|
+
navigation.navigate(route) // ROUTE INJECTION
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// React Navigation without validation
|
|
236
|
+
const config = {
|
|
237
|
+
screens: {
|
|
238
|
+
Payment: 'payment/:amount', // amount not validated!
|
|
239
|
+
Profile: 'user/:userId', // IDOR possible!
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
FIX:
|
|
244
|
+
import { Linking } from 'react-native'
|
|
245
|
+
|
|
246
|
+
// Complete deep link validation
|
|
247
|
+
const handleDeepLink = async (url: string) => {
|
|
248
|
+
// 1. Parse URL
|
|
249
|
+
let parsed: URL
|
|
250
|
+
try {
|
|
251
|
+
parsed = new URL(url)
|
|
252
|
+
} catch {
|
|
253
|
+
return // Invalid URL
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 2. Verify allowed scheme
|
|
257
|
+
const allowedSchemes = ['myapp:', 'https:']
|
|
258
|
+
if (!allowedSchemes.includes(parsed.protocol)) return
|
|
259
|
+
|
|
260
|
+
// 3. If HTTPS, verify host
|
|
261
|
+
if (parsed.protocol === 'https:') {
|
|
262
|
+
const allowedHosts = ['yourapp.com', 'app.yourapp.com']
|
|
263
|
+
if (!allowedHosts.includes(parsed.hostname)) return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 4. Route to allowed screens
|
|
267
|
+
const path = parsed.pathname.split('/').filter(Boolean)
|
|
268
|
+
|
|
269
|
+
switch (path[0]) {
|
|
270
|
+
case 'product': {
|
|
271
|
+
const productId = parseInt(path[1], 10)
|
|
272
|
+
if (isNaN(productId) || productId <= 0) return
|
|
273
|
+
navigation.navigate('Product', { id: productId })
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case 'payment': {
|
|
278
|
+
// Sensitive routes require auth
|
|
279
|
+
const session = await getCurrentSession()
|
|
280
|
+
if (!session) {
|
|
281
|
+
navigation.navigate('Login', { returnTo: url })
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
// Parameters validated server-side, not client-side
|
|
285
|
+
navigation.navigate('Payment')
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case 'reset-password': {
|
|
290
|
+
const token = parsed.searchParams.get('token')
|
|
291
|
+
// Opaque token, validated by server
|
|
292
|
+
if (!token || token.length < 32) return
|
|
293
|
+
navigation.navigate('ResetPassword', { token })
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
default:
|
|
298
|
+
// Unknown route → ignore
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
304
|
+
REACT NATIVE CERTIFICATE PINNING
|
|
305
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
306
|
+
|
|
307
|
+
HIGH PATTERN 6 — No certificate pinning
|
|
308
|
+
DETECT:
|
|
309
|
+
// Standard fetch without pinning
|
|
310
|
+
fetch('https://api.yourapp.com/users')
|
|
311
|
+
|
|
312
|
+
// Axios without pinning
|
|
313
|
+
axios.get('https://api.yourapp.com/data')
|
|
314
|
+
|
|
315
|
+
// None of the following libraries imported:
|
|
316
|
+
// react-native-ssl-pinning
|
|
317
|
+
// react-native-pinch
|
|
318
|
+
// No native pinning module
|
|
319
|
+
|
|
320
|
+
FIX:
|
|
321
|
+
// Installation
|
|
322
|
+
// yarn add react-native-ssl-pinning
|
|
323
|
+
|
|
324
|
+
import { fetch as sslFetch } from 'react-native-ssl-pinning'
|
|
325
|
+
|
|
326
|
+
// Get certificate hash:
|
|
327
|
+
// openssl s_client -connect api.yourapp.com:443 | \
|
|
328
|
+
// openssl x509 -pubkey -noout | \
|
|
329
|
+
// openssl pkey -pubin -outform der | \
|
|
330
|
+
// openssl dgst -sha256 -binary | base64
|
|
331
|
+
|
|
332
|
+
const API_PINS = [
|
|
333
|
+
'YOUR_PRIMARY_PIN_BASE64==',
|
|
334
|
+
'YOUR_BACKUP_PIN_BASE64==', // Always have a backup!
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
export const secureApi = {
|
|
338
|
+
get: async (endpoint: string, options = {}) => {
|
|
339
|
+
return sslFetch(`https://api.yourapp.com${endpoint}`, {
|
|
340
|
+
method: 'GET',
|
|
341
|
+
sslPinning: {
|
|
342
|
+
certs: API_PINS,
|
|
343
|
+
},
|
|
344
|
+
headers: {
|
|
345
|
+
'Authorization': `Bearer ${await getToken()}`,
|
|
346
|
+
'Content-Type': 'application/json',
|
|
347
|
+
},
|
|
348
|
+
...options,
|
|
349
|
+
})
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
post: async (endpoint: string, body: object) => {
|
|
353
|
+
return sslFetch(`https://api.yourapp.com${endpoint}`, {
|
|
354
|
+
method: 'POST',
|
|
355
|
+
sslPinning: {
|
|
356
|
+
certs: API_PINS,
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify(body),
|
|
359
|
+
headers: {
|
|
360
|
+
'Authorization': `Bearer ${await getToken()}`,
|
|
361
|
+
'Content-Type': 'application/json',
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// IMPORTANT: Pin rotation plan
|
|
368
|
+
// 1. Generate new pins 30 days before expiration
|
|
369
|
+
// 2. Deploy app with [old pin, new pin]
|
|
370
|
+
// 3. Wait >95% of users have migrated
|
|
371
|
+
// 4. Deploy app with [new pin] only
|
|
372
|
+
|
|
373
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
374
|
+
BACKGROUND SCREENSHOT (iOS/Android)
|
|
375
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
376
|
+
|
|
377
|
+
MEDIUM PATTERN 7 — Sensitive data visible in switcher
|
|
378
|
+
DETECT:
|
|
379
|
+
// No background screenshot protection
|
|
380
|
+
// No AppState listener to hide data
|
|
381
|
+
// No FLAG_SECURE (Android) or blurring (iOS)
|
|
382
|
+
|
|
383
|
+
FIX:
|
|
384
|
+
import { AppState, Platform } from 'react-native'
|
|
385
|
+
import { useEffect, useState } from 'react'
|
|
386
|
+
|
|
387
|
+
// Background protection hook
|
|
388
|
+
export const useBackgroundProtection = () => {
|
|
389
|
+
const [isBackground, setIsBackground] = useState(false)
|
|
390
|
+
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
const subscription = AppState.addEventListener(
|
|
393
|
+
'change',
|
|
394
|
+
(nextAppState) => {
|
|
395
|
+
setIsBackground(nextAppState === 'background' ||
|
|
396
|
+
nextAppState === 'inactive')
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
return () => subscription.remove()
|
|
401
|
+
}, [])
|
|
402
|
+
|
|
403
|
+
return isBackground
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Protection component
|
|
407
|
+
export const ProtectedScreen = ({ children }) => {
|
|
408
|
+
const isBackground = useBackgroundProtection()
|
|
409
|
+
|
|
410
|
+
if (isBackground) {
|
|
411
|
+
return (
|
|
412
|
+
<View style={styles.blurScreen}>
|
|
413
|
+
<Image source={require('./assets/logo.png')} />
|
|
414
|
+
// Masks content with app logo
|
|
415
|
+
</View>
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return children
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Android: FLAG_SECURE (prevents screenshots)
|
|
423
|
+
// In MainActivity.java:
|
|
424
|
+
// getWindow().setFlags(
|
|
425
|
+
// WindowManager.LayoutParams.FLAG_SECURE,
|
|
426
|
+
// WindowManager.LayoutParams.FLAG_SECURE
|
|
427
|
+
// );
|
|
428
|
+
|
|
429
|
+
// Library: react-native-screenshot-prevent
|
|
430
|
+
|
|
431
|
+
═══════════════════════════════════════════════════════════════
|
|
432
|
+
COMPLETE REACT NATIVE CHECKLIST
|
|
433
|
+
═══════════════════════════════════════════════════════════════
|
|
434
|
+
|
|
435
|
+
STORAGE
|
|
436
|
+
□ AsyncStorage without sensitive data ?
|
|
437
|
+
□ Keychain for tokens and passwords ?
|
|
438
|
+
□ MMKV with encryption if used ?
|
|
439
|
+
□ Redux Persist: sensitive data blacklisted ?
|
|
440
|
+
□ SQLite encrypted (SQLCipher) if local database ?
|
|
441
|
+
|
|
442
|
+
NETWORK
|
|
443
|
+
□ Certificate pinning implemented ?
|
|
444
|
+
□ Backup pins present ?
|
|
445
|
+
□ All endpoints in HTTPS ?
|
|
446
|
+
□ Tokens in headers, not query params ?
|
|
447
|
+
□ Network Security Config (Android) configured ?
|
|
448
|
+
|
|
449
|
+
WEBVIEW
|
|
450
|
+
□ URLs strictly whitelisted ?
|
|
451
|
+
□ javaScriptEnabled justified ?
|
|
452
|
+
□ WebView messages validated ?
|
|
453
|
+
□ allowFileAccess=false ?
|
|
454
|
+
□ JavaScript Bridge minimal and secure ?
|
|
455
|
+
|
|
456
|
+
CODE
|
|
457
|
+
□ console.log disabled in production ?
|
|
458
|
+
□ __DEV__ used for debug logs ?
|
|
459
|
+
□ No API keys in JS code ?
|
|
460
|
+
□ Source maps disabled in production ?
|
|
461
|
+
□ Hermes enabled (Android) ?
|
|
462
|
+
|
|
463
|
+
DEEP LINKS
|
|
464
|
+
□ All parameters validated ?
|
|
465
|
+
□ Allowed routes whitelisted ?
|
|
466
|
+
□ Auth checked before sensitive actions ?
|
|
467
|
+
□ Minimal intent filters (Android) ?
|
|
468
|
+
|
|
469
|
+
INTERFACE
|
|
470
|
+
□ Screenshot protection in background ?
|
|
471
|
+
□ Sensitive fields with secureTextEntry ?
|
|
472
|
+
□ Clipboard protection on sensitive data ?
|
|
473
|
+
□ Autocomplete disabled on sensitive fields ?
|
|
474
|
+
|
|
475
|
+
BUILD
|
|
476
|
+
□ ProGuard/R8 enabled in Android release ?
|
|
477
|
+
□ Debug builds separate from release builds ?
|
|
478
|
+
□ Signing config secured ?
|
|
479
|
+
□ Dependencies audited ?
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 🔑 AUTH MOBILE — VULNERABILITY GUIDE
|
|
2
|
+
# Authentication and session management on mobile
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
MOBILE AUTH VS WEB AUTH
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
WHAT IS DIFFERENT ON MOBILE
|
|
9
|
+
─────────────────────────────────
|
|
10
|
+
WEB MOBILE
|
|
11
|
+
────────────────────── ──────────────────────────────
|
|
12
|
+
HttpOnly Cookies Tokens in Keychain/Keystore
|
|
13
|
+
Server sessions Stateless JWT often used
|
|
14
|
+
Browser cache cleared Data persists on device
|
|
15
|
+
No physical access Device can be stolen
|
|
16
|
+
Logout = cookie cleared Logout = token actively deleted
|
|
17
|
+
MFA via web MFA via biometrics or TOTP app
|
|
18
|
+
|
|
19
|
+
MOBILE-SPECIFIC RISKS
|
|
20
|
+
───────────────────────────────
|
|
21
|
+
1. Stolen token = permanent access if no expiration
|
|
22
|
+
2. Stolen device = access if token in cleartext
|
|
23
|
+
3. Biometrics bypassable if poorly implemented
|
|
24
|
+
4. Session persists indefinitely on device
|
|
25
|
+
5. Tokens copyable between devices if not device-bound
|
|
26
|
+
|
|
27
|
+
═══════════════════════════════════════════════════════════════
|
|
28
|
+
MOBILE AUTH VULNERABILITIES
|
|
29
|
+
═══════════════════════════════════════════════════════════════
|
|
30
|
+
|
|
31
|
+
VULN-AUTH-001 — Token Stored Insecurely
|
|
32
|
+
Severity: HIGH (CVSS 7.5)
|
|
33
|
+
MASVS : MASVS-AUTH-2
|
|
34
|
+
|
|
35
|
+
The auth token = the keys to the kingdom.
|
|
36
|
+
Its storage must be handled with the same rigor
|
|
37
|
+
as a password.
|
|
38
|
+
|
|
39
|
+
See VULN-STORAGE-001 for the complete fix.
|
|
40
|
+
|
|
41
|
+
VULN-AUTH-002 — Biometrics Implemented as Simple Boolean
|
|
42
|
+
Severity: HIGH (CVSS 7.1)
|
|
43
|
+
MASVS : MASVS-AUTH-3
|
|
44
|
+
|
|
45
|
+
Explanation:
|
|
46
|
+
The vulnerable implementation:
|
|
47
|
+
1. User places finger
|
|
48
|
+
2. Biometric SDK returns true/false
|
|
49
|
+
3. If true → display sensitive data
|
|
50
|
+
|
|
51
|
+
With Frida, hook the method and always return true.
|
|
52
|
+
Even without Frida, if the app checks a simple
|
|
53
|
+
"biometric_ok=true" flag server-side, it's trivially bypassable.
|
|
54
|
+
|
|
55
|
+
The correct implementation:
|
|
56
|
+
Biometrics must unlock a CRYPTOGRAPHIC KEY
|
|
57
|
+
that is needed to decrypt data or
|
|
58
|
+
sign a server request.
|
|
59
|
+
Without the key → no access, even if the check is bypassed.
|
|
60
|
+
|
|
61
|
+
Fix:
|
|
62
|
+
// react-native-keychain — Biometrics linked to key
|
|
63
|
+
// See REM-MOB-007 in MOBILE-REMEDIATION-LIBRARY.md
|
|
64
|
+
|
|
65
|
+
VULN-AUTH-003 — No Inactivity Timeout
|
|
66
|
+
Severity: MEDIUM (CVSS 5.5)
|
|
67
|
+
MASVS : MASVS-AUTH-2
|
|
68
|
+
|
|
69
|
+
Explanation:
|
|
70
|
+
If a user leaves their phone unlocked
|
|
71
|
+
with the app open, anyone can access it.
|
|
72
|
+
|
|
73
|
+
Banking, health, and finance apps MUST
|
|
74
|
+
implement an inactivity timeout.
|
|
75
|
+
|
|
76
|
+
Fix:
|
|
77
|
+
// React Native — Inactivity timeout
|
|
78
|
+
import { AppState } from 'react-native'
|
|
79
|
+
|
|
80
|
+
const INACTIVITY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
|
|
81
|
+
|
|
82
|
+
export const useInactivityTimeout = (
|
|
83
|
+
onTimeout: () => void
|
|
84
|
+
) => {
|
|
85
|
+
const lastActivityRef = useRef(Date.now())
|
|
86
|
+
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
|
87
|
+
|
|
88
|
+
const resetTimer = useCallback(() => {
|
|
89
|
+
lastActivityRef.current = Date.now()
|
|
90
|
+
clearTimeout(timerRef.current)
|
|
91
|
+
timerRef.current = setTimeout(onTimeout, INACTIVITY_TIMEOUT)
|
|
92
|
+
}, [onTimeout])
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
// Start timer
|
|
96
|
+
resetTimer()
|
|
97
|
+
|
|
98
|
+
// Listen for return to foreground
|
|
99
|
+
const sub = AppState.addEventListener('change', state => {
|
|
100
|
+
if (state === 'active') {
|
|
101
|
+
const elapsed = Date.now() - lastActivityRef.current
|
|
102
|
+
if (elapsed > INACTIVITY_TIMEOUT) {
|
|
103
|
+
onTimeout()
|
|
104
|
+
} else {
|
|
105
|
+
resetTimer()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
sub.remove()
|
|
112
|
+
clearTimeout(timerRef.current)
|
|
113
|
+
}
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
return resetTimer // Call on every user interaction
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
VULN-AUTH-004 — Non-Revocable Refresh Token
|
|
120
|
+
Severity: HIGH (CVSS 7.2)
|
|
121
|
+
MASVS : MASVS-AUTH-2
|
|
122
|
+
|
|
123
|
+
Explanation:
|
|
124
|
+
If a refresh token is stolen and cannot be revoked:
|
|
125
|
+
→ Attacker has access as long as token is valid
|
|
126
|
+
→ Even after password change
|
|
127
|
+
→ Even after reporting the theft
|
|
128
|
+
|
|
129
|
+
Server-side fix (backend):
|
|
130
|
+
→ Store refresh tokens in database
|
|
131
|
+
→ Allow revocation (logout = DB invalidation)
|
|
132
|
+
→ Implement rotation (each use generates a new token)
|
|
133
|
+
→ Detect reuse (if old token reused → compromise)
|
|
134
|
+
|
|
135
|
+
// Rotation strategy:
|
|
136
|
+
// 1. Client sends refresh_token
|
|
137
|
+
// 2. Server checks validity in DB
|
|
138
|
+
// 3. Server generates new access_token AND new refresh_token
|
|
139
|
+
// 4. Server invalidates old refresh_token in DB
|
|
140
|
+
// 5. If old token is reused → invalidate ENTIRE family
|
|
141
|
+
// (sign of compromise)
|
|
142
|
+
|
|
143
|
+
VULN-AUTH-005 — Client Certificate (mTLS) Absent for Critical Apps
|
|
144
|
+
Severity: MEDIUM (CVSS 5.5)
|
|
145
|
+
MASVS : MASVS-AUTH-1
|
|
146
|
+
|
|
147
|
+
Context: Banking, health, government apps
|
|
148
|
+
|
|
149
|
+
Explanation:
|
|
150
|
+
Certificate pinning verifies the server.
|
|
151
|
+
mTLS (mutual TLS) ALSO allows the server
|
|
152
|
+
to verify that the request comes from YOUR app
|
|
153
|
+
on a LEGITIMATE DEVICE.
|
|
154
|
+
|
|
155
|
+
Without mTLS, any script can call your API
|
|
156
|
+
with a valid stolen token.
|
|
157
|
+
With mTLS, the device must present a client certificate.
|
|
158
|
+
|
|
159
|
+
Note: Complex implementation, reserved for critical apps.
|
|
160
|
+
Evaluate the security/complexity trade-off per context.
|