Skip to main content
Educational Purpose Only - Security misconfigurations can expose sensitive information and create attack vectors. Always follow security best practices in production.

Overview

Security misconfiguration occurs when security settings are not defined, implemented, or maintained properly. In this demo, the vulnerable version lacks critical security headers, uses a hardcoded secret key, potentially runs with debug mode enabled, and misses other important security configurations that protect against various attacks.

Severity Rating

MEDIUM to HIGH - CVSS Score: 7.0/10CWE Reference: CWE-16: Configuration, CWE-209: Information Exposure Through Error Messages

Vulnerable Configurations

The vulnerable version has multiple configuration issues:
app = Flask(__name__)
app.secret_key = 'clave_super_secreta_123'  # VULNERABLE

# Issues:
# - Hardcoded in source code
# - Committed to version control
# - Same across all environments
# - Weak/guessable value

Security Header Vulnerabilities

Risk: Allows XSS attacks and malicious script executionMissing Header:
Content-Security-Policy: default-src 'self'
Impact Without CSP:
<!-- Attacker can inject and execute scripts -->
<script src="https://evil.com/malware.js"></script>
<script>alert(document.cookie)</script>

<!-- Can load resources from anywhere -->
<iframe src="https://phishing.com"></iframe>
<img src="https://tracker.com/pixel.gif">
Allowed Attacks:
  • Inline script execution
  • External script loading
  • eval() and similar functions
  • Inline event handlers (onclick, onerror)
Risk: Enables clickjacking attacksMissing Header:
X-Frame-Options: DENY
Clickjacking Attack:
<!-- Attacker's page -->
<html>
<head>
    <style>
        #target-site {
            position: absolute;
            opacity: 0.0;  /* Invisible */
            z-index: 2;
        }
        #fake-button {
            position: absolute;
            z-index: 1;
        }
    </style>
</head>
<body>
    <!-- Invisible iframe with vulnerable site -->
    <iframe id="target-site" 
            src="https://auth-vulnerable.onrender.com/delete-account">
    </iframe>
    
    <!-- Visible fake button underneath -->
    <button id="fake-button">Click for FREE PRIZE!</button>
</body>
</html>
Result: User thinks they’re clicking “FREE PRIZE” but actually clicking “Delete Account”
Risk: Allows MIME type sniffing attacksMissing Header:
X-Content-Type-Options: nosniff
Attack Scenario:
# Vulnerable: Serving user-uploaded file
@app.route('/uploads/<filename>')
def serve_upload(filename):
    return send_file(f'uploads/{filename}')
Exploitation:
  1. Attacker uploads file named image.jpg containing JavaScript:
    alert('XSS via MIME sniffing')
    
  2. Victim visits: /uploads/image.jpg
  3. Without nosniff, browser detects JavaScript and executes it
  4. With nosniff, browser respects Content-Type header and treats as image
Risk: Allows man-in-the-middle attacks via SSL strippingMissing Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Attack - SSL Stripping:
  1. User types auth-vulnerable.onrender.com (no https://)
  2. Browser makes HTTP request
  3. Attacker intercepts and proxies connection
  4. Attacker downgrades to HTTP
  5. User sees HTTP site, no warning
  6. Credentials transmitted in plaintext
With HSTS: Browser automatically upgrades to HTTPS, preventing interception
Risk: Disables browser’s built-in XSS filterMissing Header:
X-XSS-Protection: 1; mode=block
Note: Modern browsers rely on CSP instead, but this provides defense-in-depth for older browsers
Risk: Leaks sensitive information in Referer headerMissing Header:
Referrer-Policy: strict-origin-when-cross-origin
Information Leak:
User on: https://auth-vulnerable.onrender.com/dashboard?user_id=123&token=secret
Clicks link to external site

External site receives:
Referer: https://auth-vulnerable.onrender.com/dashboard?user_id=123&token=secret

# Leaked: user_id, token, full URL structure

Error Information Disclosure

# vulnerable/app.py:42-44
except sqlite3.Error as e:
    flash(f'Error SQL: {str(e)}', 'danger')
    print(f"ERROR SQL: {e}")
What Attackers Learn:
Error SQL: near "WHERE": syntax error
→ Reveals: Database is SQLite
→ Reveals: Query structure
→ Suggests: SQL injection possible

Error SQL: no such table: users
→ Reveals: Database schema
→ Confirms: Table names

Error SQL: UNIQUE constraint failed: users.username
→ Reveals: Username exists
→ Enables: User enumeration

Secure Implementation

The secure version implements comprehensive security headers and configurations:
from dotenv import load_dotenv
import os

load_dotenv()

app = Flask(__name__)
# SECURE: Secret from environment variable
app.secret_key = os.getenv('SECRET_KEY')

if not app.secret_key:
    raise ValueError("SECRET_KEY environment variable not set!")

Security Configuration Checklist

1

Secret Management

import secrets

# Generate cryptographically secure key
secret_key = secrets.token_hex(32)
print(f"SECRET_KEY={secret_key}")
2

Security Headers

Complete Header Configuration:
@app.after_request
def security_headers(response):
    headers = {
        # Prevent MIME sniffing
        'X-Content-Type-Options': 'nosniff',
        
        # Clickjacking protection
        'X-Frame-Options': 'DENY',
        
        # XSS protection (legacy)
        'X-XSS-Protection': '1; mode=block',
        
        # Force HTTPS (1 year)
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
        
        # Content Security Policy
        'Content-Security-Policy': (
            "default-src 'self'; "
            "script-src 'self'; "
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data: https:; "
            "font-src 'self'; "
            "connect-src 'self'; "
            "frame-ancestors 'none'; "
            "base-uri 'self'; "
            "form-action 'self'"
        ),
        
        # Referrer control
        'Referrer-Policy': 'strict-origin-when-cross-origin',
        
        # Permissions policy
        'Permissions-Policy': (
            'geolocation=(), '
            'microphone=(), '
            'camera=(), '
            'payment=(), '
            'usb=()'
        ),
        
        # Cache control for sensitive pages
        'Cache-Control': 'no-store, no-cache, must-revalidate, private',
        'Pragma': 'no-cache',
        'Expires': '0'
    }
    
    for header, value in headers.items():
        response.headers[header] = value
    
    return response
3

Error Handling

import logging
from flask import jsonify

# Configure logging
logging.basicConfig(
    filename='app.log',
    level=logging.ERROR,
    format='%(asctime)s %(levelname)s: %(message)s'
)

# Custom error handlers
@app.errorhandler(404)
def not_found(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    # Log the actual error
    logging.error(f'Server Error: {error}')
    
    # Show generic message
    return render_template('500.html'), 500

@app.errorhandler(Exception)
def handle_exception(e):
    # Log full traceback
    logging.exception("Unhandled exception")
    
    # Return generic error
    if request.path.startswith('/api/'):
        return jsonify(error="Internal server error"), 500
    return render_template('500.html'), 500
4

Debug Mode Protection

import os

# Never allow debug in production
is_production = os.getenv('FLASK_ENV') == 'production'

if is_production and app.debug:
    raise ValueError("Debug mode cannot be enabled in production!")

# Additional safeguards
if __name__ == '__main__':
    # Only run dev server in development
    if is_production:
        raise ValueError("Use production WSGI server (gunicorn, uwsgi)")
    
    app.run(
        debug=(not is_production),
        host='127.0.0.1' if not is_production else '0.0.0.0'
    )
5

Rate Limiting

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri="redis://localhost:6379"
)

# Apply to login endpoint
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
    # ...

Testing Security Configuration

# Test with curl
curl -I https://auth-secure.onrender.com

# Should see:
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=31536000
# Content-Security-Policy: default-src 'self'
# etc.
Online Tools:

Production Deployment Checklist

  • Secrets: All secrets in environment variables, not code
  • Debug Mode: Debug disabled (DEBUG=False)
  • HTTPS: SSL/TLS certificate configured, HSTS enabled
  • Security Headers: All headers configured
  • Error Handling: Generic error messages, detailed logging
  • Rate Limiting: Implemented on sensitive endpoints
  • CSRF Protection: Enabled and tested
  • Session Security: Secure, HttpOnly, SameSite cookies
  • Dependencies: All packages updated, no known vulnerabilities
  • File Permissions: Restricted (no world-readable secrets)
  • Database: Least privilege user, encrypted connections
  • Logging: Centralized, no sensitive data logged
  • Monitoring: Error tracking, security event monitoring
  • Backups: Automated, encrypted, tested
  • WAF: Web Application Firewall configured (if applicable)

Compliance & Standards

OWASP Top 10

A05:2021 – Security MisconfigurationRanked #5 most critical web application security riskIncludes:
  • Missing security hardening
  • Unnecessary features enabled
  • Default accounts/passwords
  • Error handling reveals stack traces
  • Outdated software

PCI-DSS

Requirement 6.5: Secure developmentRequirement 6.6: Web application firewall or code reviewRequirement 10: Log and monitor all access

NIST Cybersecurity Framework

PR.DS-1: Data-at-rest is protectedPR.DS-2: Data-in-transit is protectedDE.CM-1: Network monitored for anomalies

CIS Controls

Control 3: Data ProtectionControl 14: Security Awareness TrainingControl 18: Application Software Security

References

Next Steps

Related security topics:

Build docs developers (and LLMs) love