mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
Add secure storage infrastructure and input validation utilities
Co-authored-by: animator <615622+animator@users.noreply.github.com>
This commit is contained in:
@@ -58,6 +58,11 @@ axios(config)
|
||||
: requestModel.hasFileInFormData
|
||||
? "// refer https://github.com/foss42/apidash/issues/293#issuecomment-1997568083 for details regarding integration\n\n"
|
||||
: "";
|
||||
|
||||
// Add security notice
|
||||
result += "// SECURITY NOTICE: Please validate all inputs and URLs before use in production\n";
|
||||
result += "// This code is generated for testing purposes\n\n";
|
||||
|
||||
var harJson = requestModelToHARJsonRequest(
|
||||
requestModel,
|
||||
useEnabled: true,
|
||||
|
||||
114
lib/services/secure_credential_storage.dart
Normal file
114
lib/services/secure_credential_storage.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
/// Service for securely storing and retrieving OAuth2 credentials
|
||||
/// Uses flutter_secure_storage for encryption keys and encrypted values
|
||||
class SecureCredentialStorage {
|
||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
|
||||
/// Generates a storage key from client credentials for OAuth2
|
||||
static String _generateStorageKey(String clientId, String tokenUrl) {
|
||||
final combined = '$clientId:$tokenUrl';
|
||||
final bytes = utf8.encode(combined);
|
||||
final hash = sha256.convert(bytes);
|
||||
return 'oauth2_${hash.toString().substring(0, 16)}';
|
||||
}
|
||||
|
||||
/// Store OAuth2 credentials securely
|
||||
static Future<void> storeOAuth2Credentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
required String credentialsJson,
|
||||
}) async {
|
||||
final key = _generateStorageKey(clientId, tokenUrl);
|
||||
await _secureStorage.write(key: key, value: credentialsJson);
|
||||
}
|
||||
|
||||
/// Retrieve OAuth2 credentials securely
|
||||
static Future<String?> retrieveOAuth2Credentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
}) async {
|
||||
final key = _generateStorageKey(clientId, tokenUrl);
|
||||
return await _secureStorage.read(key: key);
|
||||
}
|
||||
|
||||
/// Delete OAuth2 credentials
|
||||
static Future<void> deleteOAuth2Credentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
}) async {
|
||||
final key = _generateStorageKey(clientId, tokenUrl);
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
|
||||
/// Clear all OAuth2 credentials
|
||||
static Future<void> clearAllOAuth2Credentials() async {
|
||||
final allKeys = await _secureStorage.readAll();
|
||||
for (final key in allKeys.keys) {
|
||||
if (key.startsWith('oauth2_')) {
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store environment variable securely (for secrets)
|
||||
static Future<void> storeEnvironmentSecret({
|
||||
required String environmentId,
|
||||
required String variableKey,
|
||||
required String value,
|
||||
}) async {
|
||||
final key = 'env_${environmentId}_$variableKey';
|
||||
await _secureStorage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
/// Retrieve environment variable secret
|
||||
static Future<String?> retrieveEnvironmentSecret({
|
||||
required String environmentId,
|
||||
required String variableKey,
|
||||
}) async {
|
||||
final key = 'env_${environmentId}_$variableKey';
|
||||
return await _secureStorage.read(key: key);
|
||||
}
|
||||
|
||||
/// Delete environment variable secret
|
||||
static Future<void> deleteEnvironmentSecret({
|
||||
required String environmentId,
|
||||
required String variableKey,
|
||||
}) async {
|
||||
final key = 'env_${environmentId}_$variableKey';
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
|
||||
/// Clear all environment secrets for a specific environment
|
||||
static Future<void> clearEnvironmentSecrets({
|
||||
required String environmentId,
|
||||
}) async {
|
||||
final allKeys = await _secureStorage.readAll();
|
||||
final prefix = 'env_${environmentId}_';
|
||||
for (final key in allKeys.keys) {
|
||||
if (key.startsWith(prefix)) {
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if secure storage is available
|
||||
static Future<bool> isSecureStorageAvailable() async {
|
||||
try {
|
||||
await _secureStorage.read(key: '__test__');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
134
lib/utils/secure_codegen_utils.dart
Normal file
134
lib/utils/secure_codegen_utils.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
/// Security utilities for code generation
|
||||
/// Provides sanitization and validation for generated code to prevent injection attacks
|
||||
class SecureCodeGenUtils {
|
||||
/// Maximum length for any user input field
|
||||
static const int _maxFieldLength = 10000;
|
||||
|
||||
/// Validates if a field name is safe (alphanumeric and underscore only)
|
||||
static bool isValidFieldName(String name) {
|
||||
if (name.isEmpty || name.length > 255) {
|
||||
return false;
|
||||
}
|
||||
return RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$').hasMatch(name);
|
||||
}
|
||||
|
||||
/// Comprehensive JavaScript string escaping
|
||||
/// Prevents XSS and code injection in generated JavaScript code
|
||||
static String escapeJavaScript(String input) {
|
||||
if (input.length > _maxFieldLength) {
|
||||
throw SecurityException('Input exceeds maximum length');
|
||||
}
|
||||
|
||||
return input
|
||||
.replaceAll('\\', '\\\\') // Backslash
|
||||
.replaceAll('"', '\\"') // Double quote
|
||||
.replaceAll("'", "\\'") // Single quote
|
||||
.replaceAll('\n', '\\n') // Newline
|
||||
.replaceAll('\r', '\\r') // Carriage return
|
||||
.replaceAll('\t', '\\t') // Tab
|
||||
.replaceAll('\b', '\\b') // Backspace
|
||||
.replaceAll('\f', '\\f') // Form feed
|
||||
.replaceAll('<', '\\x3C') // Less than (XSS protection)
|
||||
.replaceAll('>', '\\x3E') // Greater than
|
||||
.replaceAll('&', '\\x26') // Ampersand
|
||||
.replaceAll('/', '\\/') // Forward slash
|
||||
.replaceAll('\u2028', '\\u2028') // Line separator
|
||||
.replaceAll('\u2029', '\\u2029'); // Paragraph separator
|
||||
}
|
||||
|
||||
/// HTML escaping for generated code comments
|
||||
static String escapeHtml(String input) {
|
||||
if (input.length > _maxFieldLength) {
|
||||
throw SecurityException('Input exceeds maximum length');
|
||||
}
|
||||
|
||||
return input
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
.replaceAll('/', '/');
|
||||
}
|
||||
|
||||
/// Python string escaping
|
||||
static String escapePython(String input) {
|
||||
if (input.length > _maxFieldLength) {
|
||||
throw SecurityException('Input exceeds maximum length');
|
||||
}
|
||||
|
||||
return input
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll("'", "\\'")
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll('\t', '\\t');
|
||||
}
|
||||
|
||||
/// Validate and sanitize URL
|
||||
/// Returns null if URL is invalid
|
||||
static String? sanitizeUrl(String url) {
|
||||
if (url.length > _maxFieldLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
// Only allow http and https schemes
|
||||
if (uri.scheme != 'http' && uri.scheme != 'https') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if (uri.host.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return uri.toString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that input doesn't contain dangerous patterns
|
||||
static bool containsDangerousPattern(String input) {
|
||||
// Check for common injection patterns
|
||||
final dangerousPatterns = [
|
||||
RegExp(r'<script', caseSensitive: false),
|
||||
RegExp(r'javascript:', caseSensitive: false),
|
||||
RegExp(r'onerror\s*=', caseSensitive: false),
|
||||
RegExp(r'onload\s*=', caseSensitive: false),
|
||||
RegExp(r'eval\s*\(', caseSensitive: false),
|
||||
RegExp(r'exec\s*\(', caseSensitive: false),
|
||||
];
|
||||
|
||||
for (final pattern in dangerousPatterns) {
|
||||
if (pattern.hasMatch(input)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Sanitize multiline string for code generation
|
||||
static String sanitizeMultiline(String input) {
|
||||
if (input.length > _maxFieldLength) {
|
||||
throw SecurityException('Input exceeds maximum length');
|
||||
}
|
||||
|
||||
// Remove any null bytes
|
||||
return input.replaceAll('\x00', '');
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception thrown when a security validation fails
|
||||
class SecurityException implements Exception {
|
||||
final String message;
|
||||
SecurityException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'SecurityException: $message';
|
||||
}
|
||||
102
lib/utils/secure_envvar_utils.dart
Normal file
102
lib/utils/secure_envvar_utils.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Security utility for environment variable substitution
|
||||
/// Protects against ReDoS (Regular Expression Denial of Service) attacks
|
||||
class SecureEnvVarUtils {
|
||||
// Maximum input length to prevent DoS attacks
|
||||
static const int _maxInputLength = 10000;
|
||||
|
||||
// Maximum number of variables before switching to alternative algorithm
|
||||
static const int _maxRegexComplexity = 1000;
|
||||
|
||||
/// Validates if a variable name is safe (alphanumeric, underscore, dash only)
|
||||
static bool isValidVariableName(String name) {
|
||||
if (name.isEmpty || name.length > 100) {
|
||||
return false;
|
||||
}
|
||||
return RegExp(r'^[a-zA-Z0-9_-]+$').hasMatch(name);
|
||||
}
|
||||
|
||||
/// Escapes special regex characters in a string
|
||||
static String escapeRegex(String input) {
|
||||
return input.replaceAllMapped(
|
||||
RegExp(r'[.*+?^${}()|[\]\\]'),
|
||||
(match) => '\\${match.group(0)}',
|
||||
);
|
||||
}
|
||||
|
||||
/// Safely substitute environment variables without ReDoS vulnerability
|
||||
///
|
||||
/// Validates input length and complexity before processing
|
||||
/// Uses alternative string matching for large variable sets
|
||||
static String? substituteVariablesSafe(
|
||||
String? input,
|
||||
Map<String, String> envVarMap,
|
||||
) {
|
||||
if (input == null) return null;
|
||||
if (envVarMap.keys.isEmpty) return input;
|
||||
|
||||
// Check input length to prevent DoS
|
||||
if (input.length > _maxInputLength) {
|
||||
throw SecurityException(
|
||||
'Input exceeds maximum length of $_maxInputLength characters'
|
||||
);
|
||||
}
|
||||
|
||||
// Validate all variable names before processing
|
||||
final invalidNames = envVarMap.keys.where((key) => !isValidVariableName(key));
|
||||
if (invalidNames.isNotEmpty) {
|
||||
throw SecurityException(
|
||||
'Invalid variable names found: ${invalidNames.join(', ')}'
|
||||
);
|
||||
}
|
||||
|
||||
// For large variable sets, use direct string replacement to avoid ReDoS
|
||||
if (envVarMap.keys.length > _maxRegexComplexity) {
|
||||
return _substituteWithoutRegex(input, envVarMap);
|
||||
}
|
||||
|
||||
// For reasonable sets, use regex with escaped keys
|
||||
try {
|
||||
final escapedKeys = envVarMap.keys.map(escapeRegex).join('|');
|
||||
final regex = RegExp(r'\{\{(' + escapedKeys + r')\}\}');
|
||||
|
||||
return input.replaceAllMapped(regex, (match) {
|
||||
final key = match.group(1)?.trim() ?? '';
|
||||
return envVarMap[key] ?? '{{$key}}';
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback to safe method on any error
|
||||
return _substituteWithoutRegex(input, envVarMap);
|
||||
}
|
||||
}
|
||||
|
||||
/// Alternative substitution method that doesn't use regex
|
||||
/// Safe for large variable sets
|
||||
static String _substituteWithoutRegex(
|
||||
String input,
|
||||
Map<String, String> envVarMap,
|
||||
) {
|
||||
var result = input;
|
||||
|
||||
// Sort by length descending to handle overlapping keys correctly
|
||||
final sortedEntries = envVarMap.entries.toList()
|
||||
..sort((a, b) => b.key.length.compareTo(a.key.length));
|
||||
|
||||
for (var entry in sortedEntries) {
|
||||
final pattern = '{{${entry.key}}}';
|
||||
result = result.replaceAll(pattern, entry.value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception thrown when a security validation fails
|
||||
class SecurityException implements Exception {
|
||||
final String message;
|
||||
SecurityException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'SecurityException: $message';
|
||||
}
|
||||
@@ -16,6 +16,7 @@ dependencies:
|
||||
path: packages/apidash_design_system
|
||||
carousel_slider: ^5.0.0
|
||||
code_builder: ^4.10.0
|
||||
crypto: ^3.0.3
|
||||
csv: ^6.0.0
|
||||
data_table_2: 2.5.16
|
||||
dart_style: ^3.0.1
|
||||
|
||||
Reference in New Issue
Block a user