From be64fdbeb030dbc199015dce283355dc67cf9489 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Oct 2025 19:00:27 +0000 Subject: [PATCH] Add secure storage infrastructure and input validation utilities Co-authored-by: animator <615622+animator@users.noreply.github.com> --- lib/codegen/js/axios.dart | 5 + lib/services/secure_credential_storage.dart | 114 +++++++++++++++++ lib/utils/secure_codegen_utils.dart | 134 ++++++++++++++++++++ lib/utils/secure_envvar_utils.dart | 102 +++++++++++++++ pubspec.yaml | 1 + 5 files changed, 356 insertions(+) create mode 100644 lib/services/secure_credential_storage.dart create mode 100644 lib/utils/secure_codegen_utils.dart create mode 100644 lib/utils/secure_envvar_utils.dart diff --git a/lib/codegen/js/axios.dart b/lib/codegen/js/axios.dart index e6651976..c1324d47 100644 --- a/lib/codegen/js/axios.dart +++ b/lib/codegen/js/axios.dart @@ -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, diff --git a/lib/services/secure_credential_storage.dart b/lib/services/secure_credential_storage.dart new file mode 100644 index 00000000..bdecc943 --- /dev/null +++ b/lib/services/secure_credential_storage.dart @@ -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 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 retrieveOAuth2Credentials({ + required String clientId, + required String tokenUrl, + }) async { + final key = _generateStorageKey(clientId, tokenUrl); + return await _secureStorage.read(key: key); + } + + /// Delete OAuth2 credentials + static Future deleteOAuth2Credentials({ + required String clientId, + required String tokenUrl, + }) async { + final key = _generateStorageKey(clientId, tokenUrl); + await _secureStorage.delete(key: key); + } + + /// Clear all OAuth2 credentials + static Future 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 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 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 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 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 isSecureStorageAvailable() async { + try { + await _secureStorage.read(key: '__test__'); + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/utils/secure_codegen_utils.dart b/lib/utils/secure_codegen_utils.dart new file mode 100644 index 00000000..92950c7a --- /dev/null +++ b/lib/utils/secure_codegen_utils.dart @@ -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' _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'; +} diff --git a/lib/utils/secure_envvar_utils.dart b/lib/utils/secure_envvar_utils.dart new file mode 100644 index 00000000..c1aa5a73 --- /dev/null +++ b/lib/utils/secure_envvar_utils.dart @@ -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 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 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'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 5edca283..8b44c46b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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