diff --git a/app/app.py b/app/app.py
index f15ffa3..a8fab15 100644
--- a/app/app.py
+++ b/app/app.py
@@ -26,7 +26,9 @@ def nl2br(value):
Markup: HTML-safe string with line breaks.
"""
if value:
- return markupsafe.Markup('
'.join(str(value).splitlines()))
+ # Markup('
').join() escapes each segment before joining.
+ # Markup('
'.join(...)) would mark attacker-controlled text as safe.
+ return markupsafe.Markup('
').join(str(value).splitlines())
return ''
@@ -137,9 +139,12 @@ def create_app():
HSTS is only sent in production (non-debug) to avoid breaking
local development over plain HTTP.
"""
+ # X-XSS-Protection is deliberately not set: the auditor it addressed
+ # has been removed from every current browser, and its last versions
+ # introduced vulnerabilities of their own. CSP frame-ancestors and
+ # X-Frame-Options cover the remaining ground.
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
- response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Permissions-Policy'] = (
'camera=(), microphone=(), geolocation=(), '
@@ -206,9 +211,13 @@ def create_app():
try:
db.session.execute(text('SELECT 1'))
health_data['database'] = 'connected'
- except Exception as e:
+ except Exception:
+ # Never echo the driver error: it routinely carries the host,
+ # database name and user of the connection string, and /health
+ # is unauthenticated.
+ app.logger.error('Health check: database unreachable', exc_info=True)
health_data['status'] = 'unhealthy'
- health_data['database'] = f'error: {str(e)}'
+ health_data['database'] = 'error'
return jsonify(health_data), 503
return jsonify(health_data), 200
diff --git a/app/logging_config.py b/app/logging_config.py
index 311c5e9..163d798 100644
--- a/app/logging_config.py
+++ b/app/logging_config.py
@@ -31,22 +31,31 @@ class SensitiveDataFilter(logging.Filter):
]
def filter(self, record):
- """Apply redaction to the log record's message.
-
+ """Apply redaction to the log record's fully rendered message.
+
+ The record is rendered first (msg % args) and the result stored back
+ as msg with args cleared. Redacting record.msg alone would miss almost
+ everything: this codebase logs with %s placeholders, so the sensitive
+ value lives in record.args while record.msg holds only the format
+ string.
+
Args:
record: The log record to filter.
-
+
Returns:
bool: Always True (never drops records, only redacts).
"""
- if hasattr(record, 'msg') and isinstance(record.msg, str):
- msg = record.msg
- for pattern, replacement in self.SENSITIVE_PATTERNS:
- if callable(replacement):
- msg = pattern.sub(replacement, msg)
- else:
- msg = pattern.sub(replacement, msg)
- record.msg = msg
+ try:
+ rendered = record.getMessage()
+ except Exception:
+ # A malformed format string must not lose the record entirely.
+ return True
+
+ for pattern, replacement in self.SENSITIVE_PATTERNS:
+ rendered = pattern.sub(replacement, rendered)
+
+ record.msg = rendered
+ record.args = ()
return True
@@ -128,14 +137,30 @@ def configure_logging(app):
app.logger.addHandler(app_handler)
# -------------------------------------------------------------------------
- # 4. Console Handler (for development)
+ # 4. Console Handler (always on)
# -------------------------------------------------------------------------
- if os.getenv('FLASK_DEBUG', 'false').lower() == 'true':
- console_handler = logging.StreamHandler()
- console_handler.setLevel(logging.DEBUG)
- console_handler.setFormatter(formatter)
- console_handler.addFilter(sensitive_filter)
- app.logger.addHandler(console_handler)
+ # Previously gated on FLASK_DEBUG, which meant production emitted nothing
+ # on stdout — precisely where the Pterodactyl console looks. Keep the
+ # handler unconditional and vary the level instead.
+ debug_mode = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.DEBUG if debug_mode else log_level)
+ console_handler.setFormatter(formatter)
+ console_handler.addFilter(sensitive_filter)
+ app.logger.addHandler(console_handler)
+
+ # -------------------------------------------------------------------------
+ # 5. Package logger ('app.*') — notably app.discord_bot
+ # -------------------------------------------------------------------------
+ # Modules using logging.getLogger(__name__) resolve to 'app.'.
+ # Without handlers here their INFO records were dropped entirely and
+ # WARNING+ fell through to Python's lastResort handler, unformatted.
+ package_logger = logging.getLogger('app')
+ package_logger.setLevel(log_level)
+ package_logger.propagate = False
+ for handler in (error_handler, app_handler, console_handler):
+ if handler not in package_logger.handlers:
+ package_logger.addHandler(handler)
# Log startup information
app.logger.info('Logging configured - Level: %s, Log directory: %s', log_level_name, log_dir)
diff --git a/app/nginx.conf b/app/nginx.conf
index 90b22e0..71c10ff 100644
--- a/app/nginx.conf
+++ b/app/nginx.conf
@@ -110,7 +110,8 @@ http {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
- add_header X-XSS-Protection "1; mode=block" always;
+ # X-XSS-Protection intentionally omitted: deprecated, removed from
+ # current browsers, and harmful in its last implementations.
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;