Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/codegen/js/axios.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This change is not required.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in commit d5bf4b8.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Why is this change not yet reverted.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in commit (pending).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in commit 8821e16.

var harJson = requestModelToHARJsonRequest(
requestModel,
useEnabled: true,
Expand Down
72 changes: 70 additions & 2 deletions lib/providers/js_runtime_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
late final JavascriptRuntime _runtime;
String? _currentRequestId;

// Security: Maximum script length to prevent DoS attacks
static const int _maxScriptLength = 50000; // 50KB

// Security: Dangerous JavaScript patterns that could lead to code injection
static const List<String> _dangerousPatterns = [
r'eval\s*\(',
r'Function\s*\(',
r'constructor\s*\[',
r'__proto__',
];

/// Validates user script for basic security checks
/// Returns null if valid, error message if invalid
String? _validateScript(String script) {
// Check script length to prevent DoS
if (script.length > _maxScriptLength) {
return 'Script exceeds maximum length of $_maxScriptLength characters';
}

// Check for dangerous patterns
for (final pattern in _dangerousPatterns) {
final regex = RegExp(pattern, caseSensitive: false);
if (regex.hasMatch(script)) {
return 'Script contains potentially dangerous pattern: ${pattern.replaceAll(r'\s*\(', '(').replaceAll(r'\s*\[', '[')}';
}
}

return null; // Script is valid
}

void _initialize() {
if (state.initialized) return;
_runtime = getJavascriptRuntime();
Expand Down Expand Up @@ -100,7 +130,26 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
}

final httpRequest = currentRequestModel.httpRequestModel;
final userScript = currentRequestModel.preRequestScript;
final userScript = currentRequestModel.preRequestScript!;

// Security: Validate user script before execution
final validationError = _validateScript(userScript);
if (validationError != null) {
final term = ref.read(terminalStateProvider.notifier);
term.logJs(
level: 'error',
args: ['Script validation failed', validationError],
context: 'preRequest',
contextRequestId: requestId,
);
state = state.copyWith(lastError: validationError);
// Return original request without executing the script
return (
updatedRequest: httpRequest!,
updatedEnvironment: activeEnvironment,
);
}

final requestJson = jsonEncode(httpRequest?.toJson());
final environmentJson = jsonEncode(activeEnvironment);
final dataInjection = '''
Expand Down Expand Up @@ -190,7 +239,26 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {

final httpRequest = currentRequestModel.httpRequestModel; // for future use
final httpResponse = currentRequestModel.httpResponseModel;
final userScript = currentRequestModel.postRequestScript;
final userScript = currentRequestModel.postRequestScript!;

// Security: Validate user script before execution
final validationError = _validateScript(userScript);
if (validationError != null) {
final term = ref.read(terminalStateProvider.notifier);
term.logJs(
level: 'error',
args: ['Script validation failed', validationError],
context: 'postResponse',
contextRequestId: requestId,
);
state = state.copyWith(lastError: validationError);
// Return original response without executing the script
return (
updatedResponse: httpResponse!,
updatedEnvironment: activeEnvironment,
);
}

final requestJson = jsonEncode(httpRequest?.toJson());
final responseJson = jsonEncode(httpResponse?.toJson());
final environmentJson = jsonEncode(activeEnvironment);
Expand Down
106 changes: 103 additions & 3 deletions lib/services/hive_services.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'secure_credential_storage.dart';

enum HiveBoxType { normal, lazy }

Expand Down Expand Up @@ -127,11 +128,110 @@ class HiveHandler {
environmentBox.put(kKeyEnvironmentBoxIds, ids);

dynamic getEnvironment(String id) => environmentBox.get(id);

/// Sets environment with automatic encryption of secrets
Future<void> setEnvironment(
String id, Map<String, dynamic>? environmentJson) =>
environmentBox.put(id, environmentJson);
String id, Map<String, dynamic>? environmentJson) async {
if (environmentJson == null) {
return environmentBox.put(id, null);
}

// Create a copy to avoid modifying the original
final secureEnvData = Map<String, dynamic>.from(environmentJson);

// Check if values array exists and process secrets
if (secureEnvData['values'] is List) {
final values = secureEnvData['values'] as List;

for (var i = 0; i < values.length; i++) {
final variable = values[i];

if (variable is Map &&
variable['type'] == 'secret' &&
variable['value'] != null &&
variable['value'].toString().isNotEmpty) {

// Store secret in secure storage
try {
await SecureCredentialStorage.storeEnvironmentSecret(
environmentId: id,
variableKey: variable['key'] ?? 'unknown_$i',
value: variable['value'].toString(),
);

// Replace value with placeholder in Hive
secureEnvData['values'][i] = {
...variable,
'value': '***SECURE***',
'isEncrypted': true,
};
} catch (e) {
// If secure storage fails, keep original value but log
// In production, consider proper error handling
}
}
}
}

return environmentBox.put(id, secureEnvData);
}

/// Gets environment with automatic decryption of secrets
Future<Map<String, dynamic>?> getEnvironmentSecure(String id) async {
final data = environmentBox.get(id);
if (data == null) return null;

// Create a copy to modify
final envData = Map<String, dynamic>.from(data);

Future<void> deleteEnvironment(String id) => environmentBox.delete(id);
// Process encrypted values
if (envData['values'] is List) {
final values = List.from(envData['values']);

for (var i = 0; i < values.length; i++) {
final variable = values[i];

if (variable is Map &&
variable['isEncrypted'] == true &&
variable['type'] == 'secret') {

// Retrieve secret from secure storage
try {
final decryptedValue = await SecureCredentialStorage.retrieveEnvironmentSecret(
environmentId: id,
variableKey: variable['key'] ?? 'unknown_$i',
);

if (decryptedValue != null) {
values[i] = {
...variable,
'value': decryptedValue,
'isEncrypted': false,
};
}
} catch (e) {
// If decryption fails, keep placeholder
}
}
}

envData['values'] = values;
}

return envData;
}

Future<void> deleteEnvironment(String id) async {
// Clean up secure storage for this environment
try {
await SecureCredentialStorage.clearEnvironmentSecrets(
environmentId: id,
);
} catch (e) {
// Log error but continue with deletion
}
return environmentBox.delete(id);
}

dynamic getHistoryIds() => historyMetaBox.get(kHistoryBoxIds);
Future<void> setHistoryIds(List<String>? ids) =>
Expand Down
114 changes: 114 additions & 0 deletions lib/services/secure_credential_storage.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading