Skip to content

Security: DJBsec/thirdparty-risk-platform

Security

docs/security.md

Security

Authentication

User Identity

The application uses a custom User model with a UUID primary key (id UUID). UUID PKs prevent enumeration attacks — an attacker who obtains a single user ID cannot guess adjacent IDs or infer the total user count.

Email address is the login identifier. Usernames are not used.

Password Hashing

All passwords are hashed with Argon2id via Django's django-argon2 backend:

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",  # fallback for legacy imports
]

Argon2id is the winner of the Password Hashing Competition and is resistant to GPU and side-channel attacks. The second entry in PASSWORD_HASHERS is kept only to allow migration of any legacy hashes and is never used for new passwords.

Brute-Force Protection

django-axes is configured to lock an account after 10 failed login attempts within a rolling window:

AXES_FAILURE_LIMIT = 10
AXES_COOLOFF_TIME = timedelta(hours=1)
AXES_LOCKOUT_PARAMETERS = ["ip_address", "username"]

Lockout is recorded in axes_accessattempt and also written to the audit log. Administrators can unlock accounts via Django admin.

Session Configuration

Sessions use Django's database-backed session engine. The session cookie is configured to maximise security:

SESSION_COOKIE_HTTPONLY = True        # Not accessible to JavaScript
SESSION_COOKIE_SAMESITE = "Strict"    # Never sent on cross-site requests
SESSION_COOKIE_SECURE = True          # HTTPS only (enforced in production)
SESSION_COOKIE_AGE = 8 * 60 * 60     # 8-hour maximum session lifetime
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

Sessions are stored in PostgreSQL. Invalidating a session (logout, admin kick, password change) is a single DELETE on the session row — it takes effect immediately for all tabs and devices.

Password Change Invalidation

The User model includes a password_changed_at timestamp. A future middleware will compare this timestamp against the session creation time — if password_changed_at > session created_at, the session is invalidated and the user is redirected to login.


Role-Based Access Control (RBAC)

Three roles are implemented as Django Groups. Users are assigned to groups via Django admin or data migration.

Role Description
Admin Full access. Can manage users, view audit log, manage reminder schedule.
Manager Can create and manage their own contracts. Can view all contracts. Cannot delete or manage users.
Viewer Read-only access to their own contracts only.

Permission Matrix

Permission Admin Manager Viewer
View all contracts YES YES NO
View own contracts YES YES YES
Create contract YES YES NO
Edit own contract YES YES NO
Edit any contract YES NO NO
Delete / terminate contract YES NO NO
Manage users YES NO NO
View audit log YES NO NO
Manage reminder schedule YES NO NO
Update own profile YES YES YES

"Own contract" means a contract where contract.owner == request.user.

RBAC is enforced at two layers (defense in depth):

  1. View layer: UserPassesTestMixin.test_func() checks permission before the view body executes (returns 403 if unauthorized).
  2. Service layer: assert_can_view(), assert_can_edit(), assert_can_delete() in contracts/services.py raise PermissionDenied regardless of how the view was reached.

Permission checks in templates are for UI rendering only (showing/hiding buttons) and must not be relied upon for security.


Security Controls

CSRF Protection

Django's CsrfViewMiddleware is enabled and cannot be bypassed. All state-changing operations (POST, PUT, DELETE) require a valid CSRF token. The token is delivered via a cookie and must be echoed in a form field or X-CSRFToken header.

XSS Prevention

Two layers of XSS protection:

  1. Django template auto-escaping: All template variables are HTML-escaped by default. Developers must explicitly use {{ value|safe }} to render raw HTML, which is disallowed in code review unless accompanied by a sanitisation step.
  2. bleach sanitisation: Any user-supplied content that may contain markup (e.g. contract notes) is sanitised with bleach.clean() in the service layer before persistence, using an explicit allowlist of safe tags.

SQL Injection

The Django ORM is used for all database access. Raw SQL (cursor.execute, Model.objects.raw) is prohibited except where absolutely necessary and only with parameterised queries. This is enforced via code review and linting.

Secrets Management

All secrets (database credentials, email credentials, secret key, Redis URL) are loaded from environment variables using django-environ:

env = environ.Env()
SECRET_KEY = env("DJANGO_SECRET_KEY")
DATABASES = {"default": env.db("DATABASE_URL")}

No secrets are committed to the repository. .env files are listed in .gitignore. In production, secrets are injected via the container environment or a secrets manager.

HTTP Security Headers

The following headers are set on all responses:

Header Value
X-Content-Type-Options nosniff
X-Frame-Options DENY
Strict-Transport-Security max-age=31536000; includeSubDomains (production)
Referrer-Policy same-origin
Content-Security-Policy default-src 'self' (tightened in Phase 4)

django-csp manages the Content Security Policy header. The policy is intentionally restrictive: no inline scripts, no external CDN resources.

Input Validation

All user input is validated at two levels:

  1. Django form validation: Field types, max lengths, required fields, and custom validators run before the service layer is called.
  2. Model clean() methods: Invariants such as end_date > start_date are enforced at the model level so they cannot be bypassed by direct model saves in tests or management commands.

Threat Model Summary

Threat Mitigation
Brute-force login django-axes lockout after 10 failures
Session hijacking HttpOnly, SameSite=Strict, Secure cookies; short 8hr TTL
CSRF CsrfViewMiddleware on all POST endpoints
XSS via stored content Template auto-escape + bleach sanitisation
SQL injection ORM-only access policy
Privilege escalation RBAC enforced in service layer, not just templates
Secrets in source code django-environ, .env excluded from git
Stale sessions after password change password_changed_at field on User model (middleware TBD)
Enumeration via sequential IDs UUID primary keys on all tables
Unrevocable tokens Session auth (no JWTs) — instant revocation via DB delete

There aren't any published security advisories