MQL5-Google-Onedrive/scripts/web_dashboard.py
google-labs-jules[bot] 86f9d7a45f feat(security): sanitize error responses and add secure logging in web dashboard
Replaces raw exception leakage in `scripts/web_dashboard.py` with generic "Internal Server Error" responses to prevent information disclosure. Implements `logging` module to capture full stack traces internally for debugging, ensuring no loss of diagnostic capability for admins.

Fixes potential vulnerability where internal paths or logic errors could be exposed to end users.
2026-02-22 23:05:40 +00:00

163 lines
6.2 KiB
Python

import os
import sys
import logging
from flask import Flask, render_template_string, jsonify
import markdown
import time
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Cache storage: filepath -> (mtime, html_content)
_content_cache = {}
# Constants for paths to avoid re-calculating on every request
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
README_PATH = os.path.join(BASE_DIR, '..', 'README.md')
VERIFICATION_PATH = os.path.join(BASE_DIR, '..', 'VERIFICATION.md')
# HTML Template
DASHBOARD_HTML = """
<!DOCTYPE html>
<html>
<head>
<title>MQL5 Trading Automation Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; max-width: 1000px; margin: 0 auto; padding: 20px; background: #f0f2f5; color: #1c1e21; }
.card { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
h1, h2 { color: #050505; border-bottom: 1px solid #ddd; padding-bottom: 10px; }
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; border: 1px solid #eee; }
.status-badge { display: inline-block; padding: 4px 12px; border-radius: 15px; font-weight: bold; background: #42b983; color: white; }
.nav { margin-bottom: 20px; background: #fff; padding: 10px 20px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
.nav a { margin-right: 15px; color: #1877f2; text-decoration: none; font-weight: bold; }
.nav a:hover { text-decoration: underline; }
footer { text-align: center; margin-top: 40px; color: #65676b; font-size: 0.9em; }
img { max-width: 100%; height: auto; }
table { border-collapse: collapse; width: 100%; margin-bottom: 1em; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #f8f9fa; }
.skip-link { position: absolute; top: -40px; left: 0; background: #42b983; color: white; padding: 8px; z-index: 100; transition: top 0.3s; text-decoration: none; border-radius: 0 0 8px 0; font-weight: 600; }
.skip-link:focus { top: 0; }
</style>
</head>
<body>
<a href="#status" class="skip-link">Skip to main content</a>
<div class="nav">
<a href="#status">System Status</a>
<a href="#docs">Documentation</a>
</div>
<div id="status" class="card">
<h1>System Status <span class="status-badge">ONLINE</span></h1>
<p>MQL5 Trading Automation is running.</p>
{{ html_verification|safe }}
</div>
<div id="docs" class="card">
<h2>Project Documentation</h2>
{{ html_readme|safe }}
</div>
<footer>
<p>&copy; {{ year }} MQL5 Trading Automation | Dashboard v1.0.0</p>
</footer>
</body>
</html>
"""
# Global to store compiled template
DASHBOARD_TEMPLATE = None
def get_cached_markdown(filepath):
"""
Returns the markdown content of a file converted to HTML, using a cache
that invalidates based on file modification time.
Optimization: Uses os.stat() to get mtime and check existence in one syscall.
"""
try:
# Optimization: os.stat gets existence and mtime in one call
# removing the need for separate os.path.exists() check
stat_result = os.stat(filepath)
except OSError:
return None
try:
mtime = stat_result.st_mtime
if filepath in _content_cache:
cached_mtime, cached_html = _content_cache[filepath]
if cached_mtime == mtime:
return cached_html
# Cache miss or file changed
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
html_content = markdown.markdown(content)
_content_cache[filepath] = (mtime, html_content)
return html_content
except Exception as e:
logger.error(f"Error reading/converting {filepath}: {e}")
return None
@app.route('/health')
def health_check():
"""Lightweight health check for load balancers."""
return jsonify({
"status": "healthy",
"timestamp": time.time()
})
@app.after_request
def add_security_headers(response):
"""
Add security headers to every response to protect against
XSS, Clickjacking, and other web vulnerabilities.
"""
# Content-Security-Policy: restrict sources of content
# default-src 'self': only allow content from own origin
# style-src 'self' 'unsafe-inline': allow inline styles (needed for template)
# script-src 'self': only allow scripts from own origin (blocks inline scripts in markdown)
csp = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'"
response.headers['Content-Security-Policy'] = csp
# X-Content-Type-Options: prevent MIME-sniffing
response.headers['X-Content-Type-Options'] = 'nosniff'
# X-Frame-Options: prevent clickjacking
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
# Referrer-Policy: control referrer information
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
@app.route('/')
def dashboard():
global DASHBOARD_TEMPLATE
try:
# Use pre-calculated paths
html_readme = get_cached_markdown(README_PATH) or "<p>README.md not found.</p>"
html_verification = get_cached_markdown(VERIFICATION_PATH) or "<p>VERIFICATION.md not found.</p>"
# ⚡ Performance Optimization: Compile template once instead of every request
if DASHBOARD_TEMPLATE is None:
DASHBOARD_TEMPLATE = app.jinja_env.from_string(DASHBOARD_HTML)
return DASHBOARD_TEMPLATE.render(html_readme=html_readme, html_verification=html_verification, year=2026)
except Exception as e:
# Securely log error with stack trace for admins, but don't leak details to user
logger.exception("Error rendering dashboard")
return "Internal Server Error", 500
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
print(f"Starting web dashboard on port {port}...")
app.run(host='0.0.0.0', port=port)