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,338 @@
|
|
|
1
|
+
# 🦋 FLUTTER — SECURITY AUDIT GUIDE
|
|
2
|
+
# Comprehensive audit guide for Flutter/Dart applications
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
FLUTTER THREAT MODEL
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
FLUTTER ARCHITECTURE AND SECURITY IMPLICATIONS
|
|
9
|
+
──────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
[Dart Code] → [Flutter Engine (C++)] → [Platform APIs]
|
|
12
|
+
↑ ↑ ↑
|
|
13
|
+
Business logic Rendering Native iOS/Android
|
|
14
|
+
UI + State Skia/Impeller Storage, Camera...
|
|
15
|
+
|
|
16
|
+
POINTS TO WATCH:
|
|
17
|
+
→ Dart code is compiled AOT (Ahead-of-Time)
|
|
18
|
+
= less readable than a JS bundle but analysable
|
|
19
|
+
→ Dart strings can still be extracted from the binary
|
|
20
|
+
→ Platform channels = bridge between Dart and native
|
|
21
|
+
= attack surface if poorly validated
|
|
22
|
+
→ Flutter Web = additional classic web attack surface
|
|
23
|
+
|
|
24
|
+
FLUTTER ATTACK SURFACE
|
|
25
|
+
──────────────────────────
|
|
26
|
+
LAYER 1 — LOCAL STORAGE
|
|
27
|
+
SharedPreferences, Hive, Isar, sqflite, path_provider
|
|
28
|
+
|
|
29
|
+
LAYER 2 — NETWORK
|
|
30
|
+
http, dio, chopper — no pinning by default
|
|
31
|
+
|
|
32
|
+
LAYER 3 — PLATFORM CHANNELS
|
|
33
|
+
MethodChannel, EventChannel — Dart/native bridge
|
|
34
|
+
|
|
35
|
+
LAYER 4 — WEBVIEW
|
|
36
|
+
webview_flutter — XSS surface if uncontrolled content
|
|
37
|
+
|
|
38
|
+
LAYER 5 — DART BINARY
|
|
39
|
+
Extractable strings, endpoints, business logic
|
|
40
|
+
|
|
41
|
+
═══════════════════════════════════════════════════════════════
|
|
42
|
+
VULNERABLE FLUTTER PATTERNS
|
|
43
|
+
═══════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
46
|
+
UNSECURE STORAGE
|
|
47
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
48
|
+
|
|
49
|
+
CRITICAL PATTERN 1 — SharedPreferences with sensitive data
|
|
50
|
+
DETECT:
|
|
51
|
+
final prefs = await SharedPreferences.getInstance();
|
|
52
|
+
await prefs.setString('auth_token', token);
|
|
53
|
+
await prefs.setString('password', password);
|
|
54
|
+
await prefs.setString('user_data', jsonEncode(user));
|
|
55
|
+
|
|
56
|
+
FIX:
|
|
57
|
+
// pubspec.yaml
|
|
58
|
+
// dependencies:
|
|
59
|
+
// flutter_secure_storage: ^9.0.0
|
|
60
|
+
|
|
61
|
+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
62
|
+
|
|
63
|
+
const _storage = FlutterSecureStorage(
|
|
64
|
+
aOptions: AndroidOptions(
|
|
65
|
+
encryptedSharedPreferences: true,
|
|
66
|
+
// Uses Android EncryptedSharedPreferences
|
|
67
|
+
keyCipherAlgorithm:
|
|
68
|
+
KeyCipherAlgorithm.RSA_ECB_OAEPwithSHA_256andMGF1Padding,
|
|
69
|
+
storageCipherAlgorithm:
|
|
70
|
+
StorageCipherAlgorithm.AES_GCM_NoPadding,
|
|
71
|
+
),
|
|
72
|
+
iOptions: IOSOptions(
|
|
73
|
+
accessibility: KeychainAccessibility.first_unlock_this_device,
|
|
74
|
+
// first_unlock_this_device = not in iCloud backups
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Store
|
|
79
|
+
await _storage.write(key: 'auth_token', value: token);
|
|
80
|
+
|
|
81
|
+
// Read
|
|
82
|
+
final token = await _storage.read(key: 'auth_token');
|
|
83
|
+
|
|
84
|
+
// Delete (logout)
|
|
85
|
+
await _storage.delete(key: 'auth_token');
|
|
86
|
+
|
|
87
|
+
// Delete all
|
|
88
|
+
await _storage.deleteAll();
|
|
89
|
+
|
|
90
|
+
HIGH PATTERN 2 — Hive without encryption
|
|
91
|
+
DETECT:
|
|
92
|
+
await Hive.initFlutter();
|
|
93
|
+
final box = await Hive.openBox('userData');
|
|
94
|
+
await box.put('token', authToken);
|
|
95
|
+
await box.put('user', user.toMap());
|
|
96
|
+
|
|
97
|
+
FIX:
|
|
98
|
+
import 'package:hive_flutter/hive_flutter.dart';
|
|
99
|
+
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
100
|
+
|
|
101
|
+
Future<Box> openEncryptedBox(String boxName) async {
|
|
102
|
+
const secureStorage = FlutterSecureStorage();
|
|
103
|
+
|
|
104
|
+
// Generate or retrieve encryption key
|
|
105
|
+
var encryptionKeyString =
|
|
106
|
+
await secureStorage.read(key: 'hive_key_$boxName');
|
|
107
|
+
|
|
108
|
+
late List<int> encryptionKey;
|
|
109
|
+
|
|
110
|
+
if (encryptionKeyString == null) {
|
|
111
|
+
// Generate a secure key
|
|
112
|
+
final key = Hive.generateSecureKey();
|
|
113
|
+
encryptionKey = key;
|
|
114
|
+
await secureStorage.write(
|
|
115
|
+
key: 'hive_key_$boxName',
|
|
116
|
+
value: base64UrlEncode(key),
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
encryptionKey = base64Url.decode(encryptionKeyString);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Hive.openBox(
|
|
123
|
+
boxName,
|
|
124
|
+
encryptionCipher: HiveAesCipher(encryptionKey),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Usage
|
|
129
|
+
final box = await openEncryptedBox('secure_user_data');
|
|
130
|
+
await box.put('token', token);
|
|
131
|
+
|
|
132
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
133
|
+
NETWORK — CERTIFICATE PINNING
|
|
134
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
135
|
+
|
|
136
|
+
HIGH PATTERN 3 — Dio without pinning
|
|
137
|
+
DETECT:
|
|
138
|
+
final dio = Dio();
|
|
139
|
+
// No pinning configuration
|
|
140
|
+
final response = await dio.get('/api/users');
|
|
141
|
+
|
|
142
|
+
FIX:
|
|
143
|
+
import 'package:dio/dio.dart';
|
|
144
|
+
import 'dart:io';
|
|
145
|
+
|
|
146
|
+
Dio createSecureDio() {
|
|
147
|
+
final dio = Dio(
|
|
148
|
+
BaseOptions(
|
|
149
|
+
baseUrl: 'https://api.yourapp.com',
|
|
150
|
+
connectTimeout: const Duration(seconds: 10),
|
|
151
|
+
receiveTimeout: const Duration(seconds: 30),
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Certificate pinning via custom HttpClient
|
|
159
|
+
(dio.httpClientAdapter as DefaultHttpClientAdapter)
|
|
160
|
+
.onHttpClientCreate = (client) {
|
|
161
|
+
|
|
162
|
+
client.badCertificateCallback = (
|
|
163
|
+
X509Certificate cert,
|
|
164
|
+
String host,
|
|
165
|
+
int port,
|
|
166
|
+
) {
|
|
167
|
+
// Verify SHA-256 of certificate
|
|
168
|
+
final expectedFingerprints = [
|
|
169
|
+
'AA:BB:CC:DD:...', // Primary pin
|
|
170
|
+
'EE:FF:00:11:...', // Backup pin (MANDATORY)
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
final certFingerprint = _getCertFingerprint(cert);
|
|
174
|
+
return expectedFingerprints.contains(certFingerprint);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return client;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Interceptor to add token
|
|
181
|
+
dio.interceptors.add(
|
|
182
|
+
InterceptorsWrapper(
|
|
183
|
+
onRequest: (options, handler) async {
|
|
184
|
+
final token = await _getSecureToken();
|
|
185
|
+
if (token != null) {
|
|
186
|
+
options.headers['Authorization'] = 'Bearer $token';
|
|
187
|
+
}
|
|
188
|
+
handler.next(options);
|
|
189
|
+
},
|
|
190
|
+
onError: (error, handler) {
|
|
191
|
+
// Do not log sensitive details
|
|
192
|
+
logger.error(
|
|
193
|
+
'API Error: ${error.response?.statusCode}',
|
|
194
|
+
);
|
|
195
|
+
handler.next(error);
|
|
196
|
+
},
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return dio;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
String _getCertFingerprint(X509Certificate cert) {
|
|
204
|
+
final digest = sha256.convert(cert.der);
|
|
205
|
+
return digest.bytes
|
|
206
|
+
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
|
207
|
+
.join(':');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
211
|
+
HARDCODED DART SECRETS
|
|
212
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
|
|
214
|
+
CRITICAL PATTERN 4 — Secret constants in Dart code
|
|
215
|
+
DETECT:
|
|
216
|
+
const String apiKey = 'sk-abc123...';
|
|
217
|
+
const String dbPassword = 'supersecret';
|
|
218
|
+
static const baseUrl = 'https://internal.corp.com/api';
|
|
219
|
+
|
|
220
|
+
class Config {
|
|
221
|
+
static const stripeKey = 'sk_live_...'; // CRITICAL
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
UNDERSTAND:
|
|
225
|
+
Even compiled, Dart strings are extractable:
|
|
226
|
+
strings app.aot | grep -i "api\|key\|secret\|password"
|
|
227
|
+
|
|
228
|
+
FIX:
|
|
229
|
+
// Option A: --dart-define at build time
|
|
230
|
+
// flutter build apk --dart-define=API_KEY=xxx
|
|
231
|
+
|
|
232
|
+
class Config {
|
|
233
|
+
static const apiKey =
|
|
234
|
+
String.fromEnvironment('API_KEY', defaultValue: '');
|
|
235
|
+
|
|
236
|
+
// Validate at startup
|
|
237
|
+
static void validate() {
|
|
238
|
+
if (apiKey.isEmpty) {
|
|
239
|
+
throw Exception(
|
|
240
|
+
'API_KEY must be provided via --dart-define'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Option B: Fetch from secure backend
|
|
247
|
+
// on first launch (after auth)
|
|
248
|
+
|
|
249
|
+
// Option C: flutter_dotenv for development
|
|
250
|
+
// (NEVER real keys in prod)
|
|
251
|
+
|
|
252
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
253
|
+
LOGS IN PRODUCTION
|
|
254
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
255
|
+
|
|
256
|
+
MEDIUM PATTERN 5 — print() and debugPrint() in production
|
|
257
|
+
DETECT:
|
|
258
|
+
print('User token: $token');
|
|
259
|
+
debugPrint('Auth response: $response');
|
|
260
|
+
print(user.toJson()); // PII data
|
|
261
|
+
|
|
262
|
+
FIX:
|
|
263
|
+
// lib/core/logger.dart
|
|
264
|
+
import 'package:flutter/foundation.dart';
|
|
265
|
+
|
|
266
|
+
class AppLogger {
|
|
267
|
+
static void debug(String message) {
|
|
268
|
+
if (kDebugMode) debugPrint('[DEBUG] $message');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static void info(String message) {
|
|
272
|
+
if (kDebugMode) debugPrint('[INFO] $message');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
static void warn(String message) {
|
|
276
|
+
if (kDebugMode) debugPrint('[WARN] $message');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static void error(
|
|
280
|
+
String message, [
|
|
281
|
+
Object? error,
|
|
282
|
+
StackTrace? stackTrace,
|
|
283
|
+
]) {
|
|
284
|
+
if (kDebugMode) {
|
|
285
|
+
debugPrint('[ERROR] $message');
|
|
286
|
+
if (error != null) debugPrint(error.toString());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// In production: Crashlytics without sensitive data
|
|
290
|
+
FirebaseCrashlytics.instance.recordError(
|
|
291
|
+
error ?? message,
|
|
292
|
+
stackTrace,
|
|
293
|
+
reason: message,
|
|
294
|
+
// Do not include user data
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Intentional no-op for sensitive data
|
|
299
|
+
static void sensitive(String message) {}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
═══════════════════════════════════════════════════════════════
|
|
303
|
+
COMPLETE FLUTTER CHECKLIST
|
|
304
|
+
═══════════════════════════════════════════════════════════════
|
|
305
|
+
|
|
306
|
+
STORAGE
|
|
307
|
+
□ SharedPreferences without sensitive data ?
|
|
308
|
+
□ flutter_secure_storage for tokens and secrets ?
|
|
309
|
+
□ Hive with encryption if sensitive data ?
|
|
310
|
+
□ sqflite with SQLCipher if sensitive data ?
|
|
311
|
+
□ Encryption keys in secure_storage ?
|
|
312
|
+
|
|
313
|
+
NETWORK
|
|
314
|
+
□ Dio with certificate pinning ?
|
|
315
|
+
□ Backup pin configured ?
|
|
316
|
+
□ Timeout configured (connect + receive) ?
|
|
317
|
+
□ Tokens in headers via interceptor ?
|
|
318
|
+
□ Error logs without sensitive data ?
|
|
319
|
+
|
|
320
|
+
CODE
|
|
321
|
+
□ No secret constants in Dart code ?
|
|
322
|
+
□ --dart-define used for sensitive configs ?
|
|
323
|
+
□ kDebugMode for print() ?
|
|
324
|
+
□ Release mode tested (flutter build --release) ?
|
|
325
|
+
□ ProGuard enabled for native code (Android) ?
|
|
326
|
+
|
|
327
|
+
PLATFORM
|
|
328
|
+
□ Secured AndroidManifest ?
|
|
329
|
+
□ Info.plist minimal permissions ?
|
|
330
|
+
□ flutter_secure_storage options configured ?
|
|
331
|
+
□ allowBackup=false (Android) ?
|
|
332
|
+
□ Keyboard cache disabled on sensitive fields ?
|
|
333
|
+
|
|
334
|
+
DEPENDENCIES
|
|
335
|
+
□ pub audit executed ?
|
|
336
|
+
□ Packages up to date ?
|
|
337
|
+
□ pubspec.lock committed ?
|
|
338
|
+
□ Firebase/analytics SDK: minimal data ?
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# ⚡ IONIC / CAPACITOR — SECURITY AUDIT GUIDE
|
|
2
|
+
# Comprehensive audit guide for Ionic applications
|
|
3
|
+
|
|
4
|
+
═══════════════════════════════════════════════════════════════
|
|
5
|
+
IONIC THREAT MODEL
|
|
6
|
+
═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
IONIC IS FUNDAMENTALLY DIFFERENT
|
|
9
|
+
──────────────────────────────────────
|
|
10
|
+
Ionic = Web Application (HTML/CSS/JS) in a native WebView.
|
|
11
|
+
|
|
12
|
+
This means:
|
|
13
|
+
→ ALL web vulnerabilities apply
|
|
14
|
+
→ PLUS all mobile vulnerabilities apply
|
|
15
|
+
→ XSS in a WebView = access to native APIs
|
|
16
|
+
→ The attack surface is the LARGEST of all mobile frameworks
|
|
17
|
+
|
|
18
|
+
[HTML/CSS/JS Angular/React/Vue]
|
|
19
|
+
↓
|
|
20
|
+
[WebView (WKWebView iOS / WebView Android)]
|
|
21
|
+
↓
|
|
22
|
+
[Capacitor Plugins → Native APIs]
|
|
23
|
+
↓
|
|
24
|
+
[Operating System]
|
|
25
|
+
|
|
26
|
+
XSS IN IONIC = POTENTIAL FILESYSTEM ACCESS
|
|
27
|
+
|
|
28
|
+
IONIC ATTACK SURFACE
|
|
29
|
+
────────────────────────
|
|
30
|
+
LAYER 1 — WEB (everything web-related)
|
|
31
|
+
XSS, CSP, mixed content, CORS
|
|
32
|
+
|
|
33
|
+
LAYER 2 — WEBVIEW
|
|
34
|
+
WebView configuration, local file access
|
|
35
|
+
|
|
36
|
+
LAYER 3 — CAPACITOR PLUGINS
|
|
37
|
+
FileSystem, Camera, Geolocation, Storage
|
|
38
|
+
= native access via JavaScript calls
|
|
39
|
+
|
|
40
|
+
LAYER 4 — NETWORK
|
|
41
|
+
Same issues as native mobile app
|
|
42
|
+
|
|
43
|
+
LAYER 5 — STORAGE
|
|
44
|
+
localStorage, sessionStorage, Capacitor Storage,
|
|
45
|
+
Capacitor Filesystem
|
|
46
|
+
|
|
47
|
+
═══════════════════════════════════════════════════════════════
|
|
48
|
+
VULNERABLE IONIC PATTERNS
|
|
49
|
+
═══════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
XSS → NATIVE ACCESS VIA CAPACITOR
|
|
53
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
54
|
+
|
|
55
|
+
CRITICAL PATTERN 1 — XSS + Capacitor = Mobile RCE
|
|
56
|
+
UNDERSTAND:
|
|
57
|
+
XSS in an Ionic app is not just cookie theft.
|
|
58
|
+
The attacker can call Capacitor plugins:
|
|
59
|
+
|
|
60
|
+
// From an XSS injection:
|
|
61
|
+
Filesystem.readFile({ path: '/etc/passwd' }) → file read
|
|
62
|
+
Geolocation.getCurrentPosition() → GPS tracking
|
|
63
|
+
Camera.getPhoto() → camera access
|
|
64
|
+
Capacitor.Plugins.SecureStorage.get('token') → token theft
|
|
65
|
+
|
|
66
|
+
DETECT:
|
|
67
|
+
// innerHTML without sanitisation
|
|
68
|
+
element.innerHTML = userContent
|
|
69
|
+
|
|
70
|
+
// Angular: [innerHTML] without DOMSanitizer
|
|
71
|
+
<div [innerHTML]="userContent"></div>
|
|
72
|
+
|
|
73
|
+
// Ionic with innerHtml without sanitisation
|
|
74
|
+
this.content = data.userDescription // never sanitised
|
|
75
|
+
|
|
76
|
+
FIX:
|
|
77
|
+
// Angular — DOMSanitizer mandatory
|
|
78
|
+
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
|
79
|
+
|
|
80
|
+
constructor(private sanitizer: DomSanitizer) {}
|
|
81
|
+
|
|
82
|
+
getSafeHtml(content: string): SafeHtml {
|
|
83
|
+
// Option 1: Sanitize with Angular
|
|
84
|
+
return this.sanitizer.sanitize(
|
|
85
|
+
SecurityContext.HTML,
|
|
86
|
+
content
|
|
87
|
+
) ?? ''
|
|
88
|
+
|
|
89
|
+
// Option 2: DOMPurify (recommended)
|
|
90
|
+
// npm install dompurify @types/dompurify
|
|
91
|
+
return DOMPurify.sanitize(content, {
|
|
92
|
+
ALLOWED_TAGS: ['p', 'b', 'i', 'br', 'strong', 'em'],
|
|
93
|
+
ALLOWED_ATTR: [],
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Template
|
|
98
|
+
// <div [innerHTML]="getSafeHtml(userContent)"></div>
|
|
99
|
+
|
|
100
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
101
|
+
UNSECURE IONIC STORAGE
|
|
102
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
|
+
|
|
104
|
+
HIGH PATTERN 2 — localStorage with sensitive data
|
|
105
|
+
DETECT:
|
|
106
|
+
localStorage.setItem('token', authToken)
|
|
107
|
+
localStorage.setItem('user', JSON.stringify(user))
|
|
108
|
+
sessionStorage.setItem('password', password)
|
|
109
|
+
|
|
110
|
+
// Capacitor Storage (old API, unencrypted)
|
|
111
|
+
import { Storage } from '@capacitor/storage'
|
|
112
|
+
await Storage.set({ key: 'token', value: token })
|
|
113
|
+
|
|
114
|
+
FIX:
|
|
115
|
+
// npm install @capacitor/preferences
|
|
116
|
+
// + encryption solution
|
|
117
|
+
|
|
118
|
+
// Option: Ionic Secure Storage (encrypted, with biometrics)
|
|
119
|
+
// npm install @ionic-enterprise/secure-storage
|
|
120
|
+
import { SecureStorage } from '@ionic-enterprise/secure-storage'
|
|
121
|
+
|
|
122
|
+
const storage = new SecureStorage()
|
|
123
|
+
await storage.create()
|
|
124
|
+
|
|
125
|
+
await storage.set('auth_token', token)
|
|
126
|
+
const token = await storage.get('auth_token')
|
|
127
|
+
await storage.remove('auth_token')
|
|
128
|
+
|
|
129
|
+
// Open-source alternative:
|
|
130
|
+
// npm install @aparajita/capacitor-secure-storage
|
|
131
|
+
import { SecureStorage } from
|
|
132
|
+
'@aparajita/capacitor-secure-storage'
|
|
133
|
+
|
|
134
|
+
await SecureStorage.set('token', value, true)
|
|
135
|
+
// true = requires biometrics/pin code
|
|
136
|
+
|
|
137
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
138
|
+
CONTENT SECURITY POLICY IONIC
|
|
139
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
140
|
+
|
|
141
|
+
HIGH PATTERN 3 — CSP absent or too permissive
|
|
142
|
+
DETECT in index.html:
|
|
143
|
+
<!-- No CSP at all -->
|
|
144
|
+
|
|
145
|
+
<!-- OR overly permissive CSP -->
|
|
146
|
+
<meta http-equiv="Content-Security-Policy"
|
|
147
|
+
content="default-src *">
|
|
148
|
+
|
|
149
|
+
<!-- unsafe-inline = XSS possible -->
|
|
150
|
+
<meta http-equiv="Content-Security-Policy"
|
|
151
|
+
content="default-src 'self' 'unsafe-inline'">
|
|
152
|
+
|
|
153
|
+
FIX:
|
|
154
|
+
<!-- index.html -->
|
|
155
|
+
<meta http-equiv="Content-Security-Policy" content="
|
|
156
|
+
default-src 'self';
|
|
157
|
+
|
|
158
|
+
script-src 'self';
|
|
159
|
+
|
|
160
|
+
style-src 'self' 'unsafe-inline';
|
|
161
|
+
|
|
162
|
+
img-src 'self' data: blob: https://cdn.yourapp.com;
|
|
163
|
+
|
|
164
|
+
connect-src 'self'
|
|
165
|
+
https://api.yourapp.com
|
|
166
|
+
wss://ws.yourapp.com;
|
|
167
|
+
|
|
168
|
+
font-src 'self';
|
|
169
|
+
|
|
170
|
+
object-src 'none';
|
|
171
|
+
|
|
172
|
+
base-uri 'self';
|
|
173
|
+
|
|
174
|
+
frame-ancestors 'none';
|
|
175
|
+
">
|
|
176
|
+
|
|
177
|
+
<!-- Capacitor requires capacitor: scheme -->
|
|
178
|
+
<!-- On iOS: gap: scheme may be needed -->
|
|
179
|
+
<!-- Adapt according to specific needs -->
|
|
180
|
+
|
|
181
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
182
|
+
UNSECURE CAPACITOR PLUGINS
|
|
183
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
+
|
|
185
|
+
HIGH PATTERN 4 — Filesystem plugin without validation
|
|
186
|
+
DETECT:
|
|
187
|
+
import { Filesystem, Directory } from '@capacitor/filesystem'
|
|
188
|
+
|
|
189
|
+
// Path built with user input
|
|
190
|
+
const path = `documents/${userInput}/data.json`
|
|
191
|
+
await Filesystem.readFile({ path, directory: Directory.Data })
|
|
192
|
+
|
|
193
|
+
// Path traversal possible if userInput = "../../etc/passwd"
|
|
194
|
+
|
|
195
|
+
FIX:
|
|
196
|
+
import { Filesystem, Directory } from '@capacitor/filesystem'
|
|
197
|
+
|
|
198
|
+
const safeReadFile = async (filename: string) => {
|
|
199
|
+
// Whitelist of allowed filenames
|
|
200
|
+
const allowedPattern = /^[a-zA-Z0-9_-]+\.json$/
|
|
201
|
+
|
|
202
|
+
if (!allowedPattern.test(filename)) {
|
|
203
|
+
throw new Error('Filename not allowed')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// No path traversal possible
|
|
207
|
+
// thanks to strict pattern and Directory
|
|
208
|
+
return Filesystem.readFile({
|
|
209
|
+
path: filename,
|
|
210
|
+
directory: Directory.Data, // Confined to app directory
|
|
211
|
+
encoding: Encoding.UTF8,
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
═══════════════════════════════════════════════════════════════
|
|
216
|
+
COMPLETE IONIC CHECKLIST
|
|
217
|
+
═══════════════════════════════════════════════════════════════
|
|
218
|
+
|
|
219
|
+
XSS AND OUTPUT
|
|
220
|
+
□ DOMPurify used on all user HTML content ?
|
|
221
|
+
□ Angular DomSanitizer used for [innerHTML] ?
|
|
222
|
+
□ CSP configured in index.html ?
|
|
223
|
+
□ CSP sufficiently restrictive (no unsafe-inline on script) ?
|
|
224
|
+
□ No eval() or new Function() with user input ?
|
|
225
|
+
|
|
226
|
+
STORAGE
|
|
227
|
+
□ localStorage without sensitive data ?
|
|
228
|
+
□ sessionStorage without sensitive data ?
|
|
229
|
+
□ Capacitor Preferences without sensitive data ?
|
|
230
|
+
□ Ionic Secure Storage for tokens and secrets ?
|
|
231
|
+
↑ Data encrypted at rest ?
|
|
232
|
+
|
|
233
|
+
NETWORK
|
|
234
|
+
□ HTTPS only ?
|
|
235
|
+
□ Certificate pinning configured ?
|
|
236
|
+
□ Tokens in headers (not localStorage → auto headers) ?
|
|
237
|
+
|
|
238
|
+
CAPACITOR PLUGINS
|
|
239
|
+
□ Minimal permissions in capacitor.config.ts ?
|
|
240
|
+
□ Filesystem: paths validated before use ?
|
|
241
|
+
□ Camera: justified and temporary access ?
|
|
242
|
+
□ Geolocation: justified and minimal access ?
|
|
243
|
+
|
|
244
|
+
BUILD
|
|
245
|
+
□ Production mode: ionic build --prod ?
|
|
246
|
+
□ Source maps disabled in production ?
|
|
247
|
+
□ Code minified and obfuscated ?
|
|
248
|
+
□ Capacitor sync performed before native build ?
|