# Security Architecture **Document Version:** 1.0 **Last Updated:** 2026-04-04 **Status:** Production LIVE (OVH VPS) **Diagram Type:** Security Architecture / Threat Model --- ## Overview This document provides a **comprehensive security architecture** for the Norda Biznes Partner application, covering: - **Security Zones** and trust boundaries - **Authentication & Authorization** mechanisms - **Security Controls** (CSRF, rate limiting, input validation, etc.) - **Threat Model** and attack surface analysis - **Data Security** (encryption, sensitive data handling) - **API Security** (external API integrations, API key management) - **Infrastructure Security** (network, firewall, SSH access) - **Security Monitoring** and incident response - **Security Best Practices** and compliance **Abstraction Level:** Security Architecture **Audience:** Security Engineers, DevOps, Developers, System Administrators **Purpose:** Understanding security boundaries, threat model, security controls, compliance verification **Related Documentation:** - [Authentication Flow](flows/01-authentication-flow.md) - Detailed authentication flow diagrams and implementation - [Network Topology](07-network-topology.md) - Network zones and firewall rules - [Container Diagram](02-container-diagram.md) - Application security boundaries - [Critical Configurations](08-critical-configurations.md) - SSL/TLS, secrets management --- ## Table of Contents 1. [Security Zones & Trust Boundaries](#1-security-zones--trust-boundaries) 2. [Authentication Architecture](#2-authentication-architecture) 3. [Authorization Model](#3-authorization-model) 4. [Security Controls](#4-security-controls) 5. [Threat Model & Attack Surface](#5-threat-model--attack-surface) 6. [Data Security](#6-data-security) 7. [API Security](#7-api-security) 8. [Infrastructure Security](#8-infrastructure-security) 9. [Security Monitoring](#9-security-monitoring) 10. [Incident Response](#10-incident-response) 11. [Compliance & Best Practices](#11-compliance--best-practices) 12. [Security Roadmap](#12-security-roadmap) --- ## 1. Security Zones & Trust Boundaries ### 1.1 Security Zones Diagram ```mermaid graph TB subgraph "Zone 0: Public Internet (UNTRUSTED)" Internet["🌐 Public Internet

Trust Level: NONE
Access: Anonymous users
Threat Level: HIGH

Threats:
β€’ DDoS attacks
β€’ SQL injection
β€’ XSS attacks
β€’ CSRF attacks
β€’ Brute force
β€’ Bot traffic"] end subgraph "Zone 1: Nginx Reverse Proxy (SECURITY BOUNDARY)" Nginx["πŸ”’ NGINX REVERSE PROXY
IP: 57.128.200.27

Trust Level: BOUNDARY
Controls:
β€’ SSL/TLS termination (Let's Encrypt)
β€’ HTTP β†’ HTTPS redirect
β€’ Request filtering
β€’ HSTS enforcement
β€’ Security headers

Exposed Ports: 443, 80
Proxy to: 127.0.0.1:5000"] end subgraph "Zone 2: Application Zone (TRUSTED)" AppZone["πŸ–₯️ APPLICATION SERVER
OVH VPS (57.128.200.27)

Trust Level: MEDIUM
Controls:
β€’ Flask-Login authentication
β€’ CSRF protection (Flask-WTF)
β€’ Rate limiting (Flask-Limiter)
β€’ Input sanitization
β€’ XSS prevention
β€’ SQL injection prevention (SQLAlchemy ORM)
β€’ Session security (secure cookies)

Gunicorn: 127.0.0.1:5000 (localhost only)
SSH: 22 (key-based auth)"] end subgraph "Zone 4: Data Zone (HIGHLY TRUSTED)" DataZone["πŸ—„οΈ DATABASE SERVER
IP: 57.128.200.27:5432

Trust Level: HIGH
Controls:
β€’ PostgreSQL authentication
β€’ Localhost-only binding (127.0.0.1)
β€’ Role-based access control
β€’ Connection encryption (SSL/TLS)
β€’ pg_hba.conf restrictions
β€’ Database user separation

Exposed Ports: 5432 (localhost only)
Allowed Connections: Application Zone only"] end subgraph "Zone 5: External APIs (THIRD-PARTY)" APIs["☁️ EXTERNAL APIs

Trust Level: THIRD-PARTY
Services:
β€’ Google Gemini AI
β€’ Google PageSpeed Insights
β€’ Google Places API
β€’ Microsoft Graph API
β€’ Brave Search API
β€’ KRS Open API

Controls:
β€’ API key authentication
β€’ OAuth 2.0 (MS Graph)
β€’ HTTPS/TLS 1.2+ only
β€’ Rate limiting (client-side)
β€’ API key rotation
β€’ Cost tracking"] end Internet -->|"HTTPS :443
HTTP :80"| Nginx Nginx -->|"HTTP :5000
(localhost)"| AppZone AppZone -->|"PostgreSQL :5432
(localhost)"| DataZone AppZone -->|"HTTPS
(API requests)"| APIs style Internet fill:#ff6b6b,color:#fff style Nginx fill:#f59e0b,color:#fff style AppZone fill:#10b981,color:#fff style DataZone fill:#3b82f6,color:#fff style APIs fill:#8b5cf6,color:#fff ``` ### 1.2 Trust Boundaries | Boundary | Between Zones | Security Controls | Threat Mitigation | |----------|---------------|-------------------|-------------------| | **External β†’ Proxy** | Internet β†’ Nginx | SSL termination, request filtering, HSTS | DDoS, port scanning, unauthorized access | | **Proxy β†’ Application** | Nginx β†’ Gunicorn | Localhost-only binding (127.0.0.1:5000) | Lateral movement, privilege escalation | | **Application β†’ Data** | Flask β†’ PostgreSQL | Localhost-only binding, role-based access | SQL injection, unauthorized data access | | **Application β†’ Internet** | Flask β†’ External APIs | HTTPS/TLS, API key authentication, rate limiting | API key theft, cost overrun, data leakage | ### 1.3 Network Segmentation **Production (OVH VPS):** - **57.128.200.27** - Public IP (OVH VPS, direct internet access) - **Nginx:** Port 443/80 (public, SSL termination) - **Gunicorn:** 127.0.0.1:5000 (localhost only, via nginx proxy_pass) - **PostgreSQL:** 127.0.0.1:5432 (localhost only) **Staging (on-prem):** - **10.22.68.0/24** - Internal INPI network - **85.237.177.83** - Public IP (NAT at FortiGate for staging) - FortiGate + NPM (10.22.68.250) for staging.nordabiznes.pl **OVH VPS Firewall (ufw):** ``` # Production firewall rules allow tcp/443 from ANY # HTTPS allow tcp/80 from ANY # HTTP (redirect to HTTPS) allow tcp/22 from ANY # SSH (key-based auth only) deny all other inbound ``` ### 1.4 Attack Surface **External Attack Surface (Internet-facing):** - NPM reverse proxy (10.22.68.250:443, :80) - Public endpoints: `/`, `/search`, `/company/`, `/audit/*` - Authentication endpoints: `/register`, `/login`, `/forgot-password` **Internal Attack Surface (Authenticated users):** - User dashboard: `/dashboard`, `/chat`, `/forum/*`, `/wiadomosci/*` - API endpoints: `/api/chat/*`, `/api/notifications/*` **Admin Attack Surface (Admin users only):** - Admin panels: `/admin/*` (15+ panels) - Audit management: `/api/seo/audit`, `/api/gbp/audit`, `/api/social/audit` **Reduced Attack Surface:** - PostgreSQL: Localhost-only binding (no network exposure) - Gunicorn: Localhost-only binding (127.0.0.1:5000, not exposed to internet) - SSH: Key-based authentication only (password auth disabled) --- ## 2. Authentication Architecture ### 2.1 Authentication Overview **Framework:** Flask-Login **Password Hashing:** PBKDF2:SHA256 (werkzeug.security) **Session Storage:** Server-side session cookies **Email Verification:** Required before login **Email Delivery:** Microsoft Graph API (OAuth 2.0) > **NOTE:** For detailed authentication flows (registration, login, email verification, password reset, session management), see: > [Authentication Flow Documentation](flows/01-authentication-flow.md) ### 2.2 Authentication Security Architecture ```mermaid graph TB subgraph "Authentication Layer" User["πŸ‘€ User"] Browser["🌐 Browser"] subgraph "Flask Authentication Stack" FlaskLogin["Flask-Login
(Session Management)"] CSRF["Flask-WTF
(CSRF Protection)"] RateLimit["Flask-Limiter
(Rate Limiting)"] Sanitize["Input Sanitization
(XSS Prevention)"] end subgraph "User Model & Database" UserModel["User Model
(SQLAlchemy)"] PostgreSQL["PostgreSQL
(users table)"] end subgraph "Password Security" Hash["Password Hashing
(PBKDF2:SHA256)"] TokenGen["Token Generation
(secrets.token_urlsafe)"] end subgraph "Email Verification" EmailService["Email Service
(email_service.py)"] MSGraph["Microsoft Graph API
(OAuth 2.0)"] end end User -->|"Login Request"| Browser Browser -->|"POST /login"| CSRF CSRF -->|"CSRF Valid"| RateLimit RateLimit -->|"Rate OK"| Sanitize Sanitize -->|"Sanitized Input"| FlaskLogin FlaskLogin -->|"Query User"| UserModel UserModel -->|"SELECT * FROM users"| PostgreSQL PostgreSQL -->|"User Data"| Hash Hash -->|"check_password_hash()"| FlaskLogin FlaskLogin -->|"login_user()"| Browser Browser -->|"POST /register"| TokenGen TokenGen -->|"Generate Token"| PostgreSQL PostgreSQL -->|"User Created"| EmailService EmailService -->|"Send Email (OAuth)"| MSGraph style CSRF fill:#f59e0b,color:#fff style RateLimit fill:#f59e0b,color:#fff style Sanitize fill:#f59e0b,color:#fff style Hash fill:#3b82f6,color:#fff style TokenGen fill:#3b82f6,color:#fff ``` ### 2.3 Session Security **Session Configuration:** ```python # app.py session settings app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') # 256-bit secret app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only app.config['SESSION_COOKIE_HTTPONLY'] = True # No JS access app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection ``` **Session Cookie Attributes:** - **Name:** `session` (Flask default) - **Secure:** True (HTTPS only in production) - **HttpOnly:** True (prevents XSS cookie theft) - **SameSite:** Lax (CSRF protection) - **Max-Age:** 7 days (with "remember me") - **Signed:** Yes (HMAC with SECRET_KEY) - **Encrypted:** Yes (Flask built-in) **Session Lifecycle:** 1. **Creation:** On successful login (`login_user()`) 2. **Validation:** On every request (`@login_required`) 3. **Regeneration:** On login (prevents session fixation) 4. **Expiration:** After 7 days or on logout 5. **Destruction:** On logout (`logout_user()`) **Session Security Features:** - Session fixation prevention (regenerate session ID on login) - CSRF protection via Flask-WTF (automatic token generation) - Secure cookie attributes (HttpOnly, Secure, SameSite) - Server-side session storage (encrypted data) - Automatic session cleanup (expired sessions removed) ### 2.4 Password Security **Password Requirements:** - Minimum 8 characters - At least 1 uppercase letter - At least 1 lowercase letter - At least 1 digit **Password Hashing:** - **Algorithm:** PBKDF2:SHA256 (werkzeug.security default) - **Iterations:** 600,000+ (werkzeug 3.0+ default) - **Salt:** Automatic (random salt per password) - **Hash Storage:** `users.password_hash` column (VARCHAR 255) **Password Reset Flow:** - Reset tokens: 256-bit entropy (`secrets.token_urlsafe(32)`) - Token expiry: 1 hour - Single-use tokens (cleared after successful reset) - Email enumeration prevention (always show success message) **Password Best Practices:** - βœ… Strong hashing algorithm (PBKDF2:SHA256) - βœ… Password complexity requirements - βœ… Secure token generation - βœ… Token expiry enforcement - ❌ Password history (not implemented) - ❌ Account lockout after N failed attempts (not implemented) --- ## 3. Authorization Model ### 3.1 Role-Based Access Control (RBAC) ```mermaid graph TB subgraph "Authorization Hierarchy" Public["🌐 Public
Anonymous Users

Permissions:
β€’ View company directory
β€’ Search companies
β€’ View audit reports
β€’ Access registration/login"] Auth["πŸ” Authenticated
Logged-in Users

Permissions:
β€’ All Public permissions
β€’ Access dashboard
β€’ Use AI chat
β€’ Participate in forum
β€’ Send/receive messages
β€’ RSVP to events
β€’ Post classifieds"] Member["πŸ‘” NORDA Members
is_norda_member=TRUE

Permissions:
β€’ All Authenticated permissions
β€’ Company profile editing (own)
β€’ Request recommendations
β€’ View member directory
β€’ Access member-only content"] Admin["πŸ‘¨β€πŸ’Ό Administrators
is_admin=TRUE

Permissions:
β€’ All Member permissions
β€’ Manage all companies
β€’ Moderate forum/news
β€’ Manage users
β€’ Run audit scans
β€’ View analytics
β€’ Generate fees
β€’ System configuration"] end Public --> Auth Auth --> Member Member --> Admin style Public fill:#9ca3af,color:#fff style Auth fill:#10b981,color:#fff style Member fill:#3b82f6,color:#fff style Admin fill:#8b5cf6,color:#fff ``` ### 3.2 User Roles & Flags **User Model Fields:** ```sql is_active BOOLEAN DEFAULT TRUE -- User account status is_verified BOOLEAN DEFAULT FALSE -- Email verification status is_admin BOOLEAN DEFAULT FALSE -- Administrator flag is_norda_member BOOLEAN DEFAULT FALSE -- NORDA Biznes membership ``` **Role Determination Logic:** ```python # Public access - no check required # Authenticated access @login_required def protected_route(): # current_user is automatically available via Flask-Login pass # NORDA Member access @login_required def member_route(): if not current_user.is_norda_member: flash('Tylko dla czΕ‚onkΓ³w NORDA Biznes.', 'error') return redirect(url_for('index')) pass # Admin access @login_required def admin_route(): if not current_user.is_admin: flash('Brak uprawnieΕ„ administratora.', 'error') return redirect(url_for('index')) pass ``` ### 3.3 Access Control Matrix | Route Category | Public | Authenticated | NORDA Member | Admin | |---------------|--------|---------------|--------------|-------| | **Public Pages** | | | | | | `/` (Company directory) | βœ… | βœ… | βœ… | βœ… | | `/search` | βœ… | βœ… | βœ… | βœ… | | `/company/` | βœ… | βœ… | βœ… | βœ… | | `/audit/*/` (SEO, Social, GBP, IT) | βœ… | βœ… | βœ… | βœ… | | `/api/companies` (JSON export) | βœ… | βœ… | βœ… | βœ… | | `/health` (Health check) | βœ… | βœ… | βœ… | βœ… | | **Authentication** | | | | | | `/register`, `/login` | βœ… | ❌ | ❌ | ❌ | | `/logout` | ❌ | βœ… | βœ… | βœ… | | `/verify-email/` | βœ… | βœ… | βœ… | βœ… | | `/forgot-password`, `/reset-password/` | βœ… | βœ… | βœ… | βœ… | | **User Features** | | | | | | `/dashboard` | ❌ | βœ… | βœ… | βœ… | | `/chat` | ❌ | βœ… | βœ… | βœ… | | `/api/chat/*` | ❌ | βœ… | βœ… | βœ… | | **Community Features** | | | | | | `/forum/*` | ❌ | βœ… | βœ… | βœ… | | `/wiadomosci/*` (Messages) | ❌ | βœ… | βœ… | βœ… | | `/kalendarz/*` (Events) | ❌ | βœ… | βœ… | βœ… | | `/tablica/*` (Classifieds) | ❌ | βœ… | βœ… | βœ… | | **Admin Panels** | | | | | | `/admin/*` (All admin routes) | ❌ | ❌ | ❌ | βœ… | | `/admin/users` (User management) | ❌ | ❌ | ❌ | βœ… | | `/admin/fees` (Fee management) | ❌ | ❌ | ❌ | βœ… | | `/admin/forum` (Forum moderation) | ❌ | ❌ | ❌ | βœ… | | `/admin/seo` (SEO dashboard) | ❌ | ❌ | ❌ | βœ… | | `/admin/gbp-audit` (GBP dashboard) | ❌ | ❌ | ❌ | βœ… | | `/admin/social-media` (Social dashboard) | ❌ | ❌ | ❌ | βœ… | | `/admin/it-audit` (IT dashboard) | ❌ | ❌ | ❌ | βœ… | | **Admin API Endpoints** | | | | | | `/api/seo/audit` (POST) | ❌ | ❌ | ❌ | βœ… | | `/api/gbp/audit` (POST) | ❌ | ❌ | ❌ | βœ… | | `/api/social/audit` (POST) | ❌ | ❌ | ❌ | βœ… | | `/api/notifications` (GET) | ❌ | βœ… | βœ… | βœ… | | `/api/notifications//read` (PUT) | ❌ | βœ… | βœ… | βœ… | **Legend:** - βœ… Access granted - ❌ Access denied (redirect to login or show error) ### 3.4 Authorization Enforcement **Decorator-based Protection:** ```python # Flask-Login decorator for authenticated routes @app.route('/dashboard') @login_required def dashboard(): # current_user is guaranteed to be authenticated return render_template('dashboard.html', user=current_user) ``` **Manual Authorization Checks:** ```python # Admin-only check @app.route('/admin/users') @login_required def admin_users(): if not current_user.is_admin: flash('Brak uprawnieΕ„ administratora.', 'error') return redirect(url_for('index')) # Admin logic here users = db.query(User).all() return render_template('admin/users.html', users=users) ``` **Row-Level Security (Planned):** - Company owners can edit only their own company profile - Forum authors can edit/delete only their own posts - Message senders/receivers can view only their own messages **Current Limitations:** - ❌ No granular permissions (e.g., "can_edit_company", "can_moderate_forum") - ❌ No role hierarchy (custom roles beyond Admin/Member) - ❌ No permission delegation (temporary admin access) --- ## 4. Security Controls ### 4.1 Security Controls Overview ```mermaid graph LR subgraph "Input Security" Input["User Input"] Sanitize["Input Sanitization
(sanitize_input)"] Validate["Input Validation
(length, format, type)"] end subgraph "Request Security" CSRF["CSRF Protection
(Flask-WTF)"] RateLimit["Rate Limiting
(Flask-Limiter)"] Auth["Authentication
(Flask-Login)"] end subgraph "Response Security" XSS["XSS Prevention
(Jinja2 auto-escape)"] Headers["Security Headers
(HSTS, CSP, X-Frame)"] SSL["SSL/TLS Encryption
(HTTPS)"] end subgraph "Data Security" SQLInj["SQL Injection Prevention
(SQLAlchemy ORM)"] PassHash["Password Hashing
(PBKDF2:SHA256)"] Secrets["Secrets Management
(.env files)"] end Input --> Sanitize Sanitize --> Validate Validate --> CSRF CSRF --> RateLimit RateLimit --> Auth Auth --> XSS XSS --> Headers Headers --> SSL Auth --> SQLInj SQLInj --> PassHash PassHash --> Secrets style Sanitize fill:#f59e0b,color:#fff style CSRF fill:#f59e0b,color:#fff style RateLimit fill:#f59e0b,color:#fff style XSS fill:#f59e0b,color:#fff style SQLInj fill:#3b82f6,color:#fff style PassHash fill:#3b82f6,color:#fff ``` ### 4.2 Input Security #### 4.2.1 Input Sanitization **Function:** `sanitize_input(text, max_length=1000)` **Purpose:** Remove HTML tags, malicious patterns, excessive whitespace **Implementation:** `app.py` (lines ~300-320) ```python def sanitize_input(text, max_length=1000): """Remove HTML tags and malicious patterns from user input""" if not text: return "" # Remove HTML tags text = re.sub(r'<[^>]+>', '', text) # Remove script tags and content text = re.sub(r']*>.*?', '', text, flags=re.IGNORECASE | re.DOTALL) # Remove event handlers (onclick, onerror, etc.) text = re.sub(r'\s*on\w+\s*=\s*["\'][^"\']*["\']', '', text, flags=re.IGNORECASE) # Remove javascript: URLs text = re.sub(r'javascript:', '', text, flags=re.IGNORECASE) # Normalize whitespace text = ' '.join(text.split()) # Truncate to max length return text[:max_length] ``` **Applied to:** - All form inputs (registration, login, forum posts, messages, classifieds) - Search queries - Chat messages - Admin inputs **XSS Prevention:** - Jinja2 automatic escaping (enabled by default) - Manual escaping for user-generated content: `{{ user_input|e }}` - Sanitization before database storage (defense in depth) #### 4.2.2 Input Validation **Email Validation:** ```python def validate_email(email): """Validate email format""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None ``` **Password Validation:** ```python def validate_password(password): """Validate password complexity""" if len(password) < 8: return False, "Hasło musi mieć minimum 8 znaków" if not re.search(r'[A-Z]', password): return False, "Hasło musi zawierać wielką literę" if not re.search(r'[a-z]', password): return False, "Hasło musi zawierać małą literę" if not re.search(r'\d', password): return False, "Hasło musi zawierać cyfrę" return True, "" ``` **NIP Validation:** ```python def validate_nip(nip): """Validate Polish NIP format (10 digits)""" if not nip: return False nip = nip.replace('-', '').replace(' ', '') return len(nip) == 10 and nip.isdigit() ``` **Input Length Limits:** - Email: 255 characters - Password: 255 characters (hash stored) - Name: 255 characters - Phone: 50 characters - Message body: 5,000 characters - Forum post: 10,000 characters - Chat message: 1,000 characters ### 4.3 Request Security #### 4.3.1 CSRF Protection **Framework:** Flask-WTF **Implementation:** Automatic CSRF token generation and validation **Configuration:** ```python # app.py CSRF setup from flask_wtf.csrf import CSRFProtect csrf = CSRFProtect(app) app.config['WTF_CSRF_ENABLED'] = True app.config['WTF_CSRF_TIME_LIMIT'] = None # No time limit (session-based) ``` **Usage in Templates:** ```html
{{ csrf_token() }}
``` **AJAX Requests:** ```javascript // Get CSRF token from meta tag const csrfToken = document.querySelector('meta[name="csrf-token"]').content; // Include in AJAX request headers fetch('/api/endpoint', { method: 'POST', headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` **CSRF Protection Coverage:** - βœ… All POST/PUT/DELETE requests - βœ… Form submissions - βœ… AJAX API calls - ❌ GET requests (idempotent, no CSRF protection needed) #### 4.3.2 Rate Limiting **Framework:** Flask-Limiter **Strategy:** IP-based rate limiting **Configuration:** ```python # app.py rate limiting setup 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="memory://" # In-memory storage (single server) ) ``` **Rate Limits by Endpoint:** | Endpoint | Rate Limit | Purpose | |----------|------------|---------| | `/register` | 5 per hour | Prevent account spam | | `/login` | 5 per hour (prod)
1000 per hour (dev) | Prevent brute force | | `/forgot-password` | 5 per hour | Prevent email spam | | `/api/chat//message` | 30 per minute | Prevent chat spam | | `/api/seo/audit` | 10 per hour | Prevent quota abuse | | `/api/gbp/audit` | 10 per hour | Prevent quota abuse | | `/api/social/audit` | 10 per hour | Prevent quota abuse | | Default | 200 per day, 50 per hour | General protection | **Rate Limit Bypass:** - No bypass mechanism (applies to all users, including admins) - Future improvement: Whitelist admin IPs **Error Handling:** ```python @app.errorhandler(429) def ratelimit_handler(e): flash('Zbyt wiele ΕΌΔ…daΕ„. SprΓ³buj ponownie pΓ³ΕΊniej.', 'error') return render_template('error.html', error=e), 429 ``` ### 4.4 Response Security #### 4.4.1 Security Headers **HTTP Security Headers (Configured in nginx):** ``` # HSTS (HTTP Strict Transport Security) Strict-Transport-Security: max-age=31536000; includeSubDomains # Prevent clickjacking X-Frame-Options: SAMEORIGIN # XSS Protection (legacy browsers) X-XSS-Protection: 1; mode=block # Content-Type sniffing prevention X-Content-Type-Options: nosniff # Referrer Policy Referrer-Policy: strict-origin-when-cross-origin # Content Security Policy (CSP) - PLANNED # Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; ``` **Current Status:** - βœ… HSTS enabled (nginx configuration) - βœ… X-Frame-Options: SAMEORIGIN - βœ… X-Content-Type-Options: nosniff - ❌ Content-Security-Policy (not yet implemented - requires frontend refactoring) #### 4.4.2 SSL/TLS Encryption **Configuration:** - **Certificate Provider:** Let's Encrypt (free, auto-renewal) - **Certificate Type:** RSA 2048-bit - **TLS Protocols:** TLS 1.2, TLS 1.3 only - **HTTP β†’ HTTPS Redirect:** Enforced at nginx - **HSTS:** Enabled (max-age=31536000) **Cipher Suites (Modern):** ``` TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ``` **SSL Termination:** At nginx on OVH VPS (57.128.200.27) **Internal Communication:** HTTP (nginx β†’ 127.0.0.1:5000) **Certificate Renewal:** - Automatic via certbot + Let's Encrypt (on OVH VPS) - Renewal frequency: Every 90 days - Grace period: 30 days before expiry ### 4.5 Data Security #### 4.5.1 SQL Injection Prevention **ORM Usage:** SQLAlchemy (parameterized queries) **Safe Query Patterns:** ```python # βœ… SAFE: Parameterized query (SQLAlchemy ORM) user = db.query(User).filter(User.email == email).first() # βœ… SAFE: Parameterized raw SQL db.execute(text("SELECT * FROM users WHERE email = :email"), {"email": email}) # ❌ UNSAFE: String concatenation (NEVER USE) # db.execute(f"SELECT * FROM users WHERE email = '{email}'") ``` **Full-Text Search (PostgreSQL):** ```python # βœ… SAFE: FTS with plainto_tsquery (auto-escaping) query = text(""" SELECT c.*, ts_rank(c.search_vector, plainto_tsquery('english', :query)) AS rank FROM companies c WHERE c.search_vector @@ plainto_tsquery('english', :query) ORDER BY rank DESC """) results = db.execute(query, {"query": search_term}).fetchall() ``` **No Raw SQL Concatenation:** - All queries use parameterized placeholders (`:param_name`) - SQLAlchemy ORM handles escaping automatically - Full-text search uses `plainto_tsquery()` (auto-escapes special characters) #### 4.5.2 Password Storage **Hashing Algorithm:** PBKDF2:SHA256 **Library:** `werkzeug.security` **Iterations:** 600,000+ (werkzeug 3.0+ default) **Salt:** Automatic random salt per password **Implementation:** ```python from werkzeug.security import generate_password_hash, check_password_hash # Registration: Hash password password_hash = generate_password_hash(password) # Auto-salted # Login: Verify password if check_password_hash(user.password_hash, password): # Password correct login_user(user) ``` **Hash Format:** ``` pbkdf2:sha256:600000$$ ``` **Security Properties:** - βœ… Salted (prevents rainbow table attacks) - βœ… Slow hashing (prevents brute force) - βœ… Industry-standard algorithm (PBKDF2) - βœ… Automatic algorithm upgrades (werkzeug handles) #### 4.5.3 Secrets Management **Environment Variables (.env):** ```bash # Database DATABASE_URL=postgresql://nordabiz_app:PASSWORD@127.0.0.1:5432/nordabiz # Flask SECRET_KEY=<256-bit-random-key> # API Keys GOOGLE_GEMINI_API_KEY= GOOGLE_PAGESPEED_API_KEY= BRAVE_SEARCH_API_KEY= # Microsoft Graph OAuth MS_CLIENT_ID= MS_CLIENT_SECRET= MS_TENANT_ID= # Email SMTP_HOST=smtp.office365.com SMTP_PORT=587 SMTP_USER=noreply@nordabiznes.pl SMTP_PASSWORD= ``` **Secrets Storage:** - **Development:** `.env` file (gitignored) - **Production:** `.env` file on server (owned by www-data, mode 600) - **Never committed to Git:** `.env` in `.gitignore` **Secret Rotation:** - ❌ No automatic rotation (manual process) - ❌ No secret versioning (future improvement) **Access Control:** ```bash # Production secrets file permissions -rw------- 1 www-data www-data .env # Mode 600 (owner read/write only) ``` --- ## 5. Threat Model & Attack Surface ### 5.1 Threat Model (STRIDE) | Threat Type | Attack Vector | Impact | Mitigation | Status | |-------------|---------------|--------|------------|--------| | **Spoofing** | User impersonation, session hijacking | High | HTTPS, secure cookies, session regeneration | βœ… Mitigated | | **Tampering** | SQL injection, XSS, CSRF | High | SQLAlchemy ORM, input sanitization, CSRF tokens | βœ… Mitigated | | **Repudiation** | User denies actions | Medium | Database audit logs (created_at, updated_at) | ⚠️ Partial | | **Information Disclosure** | API key theft, database breach, session theft | High | HTTPS, .env files, localhost DB binding | βœ… Mitigated | | **Denial of Service** | DDoS, resource exhaustion, API quota abuse | Medium | Rate limiting, Fortigate DDoS protection | ⚠️ Partial | | **Elevation of Privilege** | Admin privilege escalation | High | Role-based access control, admin flag validation | βœ… Mitigated | ### 5.2 Attack Surface Analysis #### 5.2.1 External Attack Surface (Internet-facing) **Nginx Reverse Proxy (57.128.200.27:443, :80):** - **Exposed Services:** HTTPS (443), HTTP (80, redirects to HTTPS) - **Attack Vectors:** - DDoS attacks (mitigated by OVH DDoS protection) - SSL/TLS vulnerabilities (mitigated by modern cipher suites) - HTTP request smuggling (mitigated by nginx validation) - **Mitigation:** - OVH network-level DDoS protection - Let's Encrypt TLS 1.2/1.3 only - ufw firewall on VPS - nginx request filtering **Public Endpoints:** - `/` (Company directory) - `/search` (Search functionality) - `/company/` (Company profiles) - `/audit/*/` (SEO, Social, GBP, IT audit reports) - `/register`, `/login`, `/forgot-password` (Authentication) **Attack Vectors:** - SQL injection: **Mitigated** (SQLAlchemy ORM) - XSS: **Mitigated** (input sanitization, Jinja2 auto-escape) - CSRF: **Mitigated** (Flask-WTF) - Brute force: **Mitigated** (rate limiting 5/hour) - Account enumeration: **Partially mitigated** (generic error messages) #### 5.2.2 Internal Attack Surface (Authenticated Users) **User Dashboard & Features:** - `/dashboard`, `/chat`, `/forum/*`, `/wiadomosci/*`, `/kalendarz/*`, `/tablica/*` **Attack Vectors:** - Privilege escalation: **Mitigated** (admin flag validation) - IDOR (Insecure Direct Object Reference): **Partially mitigated** (owner checks needed) - Session hijacking: **Mitigated** (secure cookies, HTTPS) **Vulnerabilities:** - ❌ No row-level security (users can view other users' messages if they know the ID) - ❌ No rate limiting on message sending - ❌ No spam detection on forum posts #### 5.2.3 Admin Attack Surface (Admin Users) **Admin Panels:** - `/admin/*` (15+ admin panels) - `/api/seo/audit`, `/api/gbp/audit`, `/api/social/audit` (POST) **Attack Vectors:** - Admin account compromise: **Mitigated** (strong password requirements) - API quota abuse: **Mitigated** (rate limiting 10/hour) - Cost overrun: **Mitigated** (client-side cost tracking, free tier monitoring) **Vulnerabilities:** - ❌ No 2FA for admin accounts - ❌ No admin activity logging - ❌ No admin session timeout (7-day session) #### 5.2.4 Database Attack Surface **PostgreSQL (57.128.200.27:5432):** - **Binding:** Localhost only (127.0.0.1) - **Authentication:** Password-based (pg_hba.conf) - **Encryption:** SSL/TLS for connections (planned) **Attack Vectors:** - SQL injection: **Mitigated** (SQLAlchemy ORM) - Unauthorized access: **Mitigated** (localhost binding, password auth) - Data breach: **Partially mitigated** (no database encryption at rest) **Vulnerabilities:** - ❌ No database encryption at rest - ❌ No database activity auditing - ❌ No automated backup verification #### 5.2.5 API Attack Surface (External APIs) **Outbound API Calls:** - Google Gemini AI, PageSpeed Insights, Places API - Microsoft Graph API - Brave Search API - KRS Open API **Attack Vectors:** - API key theft: **Mitigated** (.env files, HTTPS only) - Man-in-the-middle: **Mitigated** (HTTPS/TLS 1.2+) - API quota abuse: **Mitigated** (rate limiting, cost tracking) **Vulnerabilities:** - ❌ API keys stored in plaintext (.env files) - ❌ No API key rotation policy - ❌ No secrets vault (e.g., HashiCorp Vault) ### 5.3 Common Attack Scenarios #### 5.3.1 SQL Injection Attack **Attack:** ```python # Attacker input: email = "admin' OR '1'='1" # UNSAFE query (NOT USED in our app): # query = f"SELECT * FROM users WHERE email = '{email}'" # Result: Returns all users (authentication bypass) ``` **Defense:** ```python # βœ… SAFE: SQLAlchemy parameterized query user = db.query(User).filter(User.email == email).first() # SQLAlchemy escapes special characters automatically ``` **Status:** βœ… Mitigated (no raw SQL concatenation) #### 5.3.2 XSS Attack **Attack:** ```html ``` **Defense:** ```python # Input sanitization (before storage) sanitized_post = sanitize_input(user_post) # Strips