forked from ComputerTech/aprhodite
Add detailed README with full security audit
This commit is contained in:
parent
c514c5fb73
commit
1c17a9bcf0
|
|
@ -0,0 +1,592 @@
|
|||
# Aphrodite — SexyChat
|
||||
|
||||
A real-time encrypted chat application built with Flask-SocketIO, featuring a public lobby, end-to-end encrypted private messaging, an AI companion ("Violet") powered by Ollama, moderation tools, and a payment-gated premium tier.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Configuration](#configuration)
|
||||
- [Installation & Setup](#installation--setup)
|
||||
- [Running the Server](#running-the-server)
|
||||
- [Socket Protocol](#socket-protocol)
|
||||
- [REST API Endpoints](#rest-api-endpoints)
|
||||
- [Encryption Design](#encryption-design)
|
||||
- [Moderation System](#moderation-system)
|
||||
- [Violet AI Companion](#violet-ai-companion)
|
||||
- [Payment & Premium Access](#payment--premium-access)
|
||||
- [Database Schema](#database-schema)
|
||||
- [Known Security Issues & Bugs](#known-security-issues--bugs)
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Public Lobby** — Ephemeral group chat room. Messages are never persisted; they live only in connected clients' browsers.
|
||||
- **User Registration & Login** — Accounts with bcrypt-hashed passwords, JWT session tokens, and moderator-gated verification.
|
||||
- **End-to-End Encrypted Private Messages** — AES-GCM-256 encryption using PBKDF2-derived keys. The server stores only ciphertext and nonces.
|
||||
- **Violet AI Companion** — A flirtatious AI hostess backed by a local Ollama instance. Messages are transit-encrypted (decrypted server-side for inference, re-encrypted before storage and delivery).
|
||||
- **Freemium AI Access** — Free users get a configurable number of AI messages before hitting a paywall. Paying users get unlimited access.
|
||||
- **Moderation Tools** — Kick, ban, kickban, mute/unmute, and manual account verification. Moderator access is granted via a shared admin password at login.
|
||||
- **Ignore System** — Registered users can ignore/unignore other users. Ignored users' messages are hidden client-side, and PM invitations from ignored users are silently blocked.
|
||||
- **Responsive UI** — Glassmorphism design with a deep purple/neon magenta theme. Mobile-friendly with a collapsible nicklist sidebar.
|
||||
- **Process Manager** — `start.py` wraps Gunicorn with daemon management (start, stop, restart, status, debug).
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌──────────────┐ WebSocket / HTTP ┌─────────────────────┐
|
||||
│ Browser │ ◄──────────────────────────────►│ Flask-SocketIO │
|
||||
│ (chat.js) │ │ (app.py) │
|
||||
│ (crypto.js) │ │ │
|
||||
└──────────────┘ │ ┌───────────────┐ │
|
||||
│ │ REST Blueprint │ │
|
||||
│ │ (routes.py) │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌───────▼───────┐ │
|
||||
│ │ SQLAlchemy │ │
|
||||
│ │ (database.py) │ │
|
||||
│ │ (models.py) │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌───────▼───────┐ │
|
||||
│ │ SQLite / PG │ │
|
||||
│ └───────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ Ollama (AI) │ │
|
||||
│ └───────────────┘ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Key design decisions:**
|
||||
|
||||
- **Single-process, single-worker** — Eventlet async mode with one Gunicorn worker. All in-memory state (connected users, bans, mutes) lives in the process.
|
||||
- **AI inference queue** — A dedicated eventlet greenlet serialises Ollama requests one at a time, broadcasting `violet_typing` indicators while busy.
|
||||
- **Lobby is ephemeral** — No lobby messages are ever written to the database.
|
||||
- **PMs are persisted for registered users** — Encrypted messages are stored in the `messages` table. History is capped at 500 messages per conversation pair.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Backend framework | Flask 3.x |
|
||||
| Real-time transport | Flask-SocketIO 5.x (eventlet async) |
|
||||
| WSGI server | Gunicorn 21.x with eventlet worker |
|
||||
| Database ORM | Flask-SQLAlchemy 3.x |
|
||||
| Migrations | Flask-Migrate 4.x (Alembic) |
|
||||
| Database | SQLite (default) or PostgreSQL (via `DATABASE_URL`) |
|
||||
| Password hashing | bcrypt 4.x |
|
||||
| JWT auth | PyJWT 2.x |
|
||||
| Server-side crypto | cryptography 42.x (AES-GCM) |
|
||||
| Client-side crypto | Web Crypto API (SubtleCrypto) |
|
||||
| AI inference | Ollama (local, HTTP API) |
|
||||
| Frontend | Vanilla JS, Socket.IO 4 client, custom CSS |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
aprhodite/
|
||||
├── app.py # Flask-SocketIO app factory, socket event handlers, AI queue
|
||||
├── database.py # SQLAlchemy & Flask-Migrate initialisation, DB seeding
|
||||
├── models.py # ORM models: User, Message, UserIgnore
|
||||
├── routes.py # REST API blueprint (auth, PM history, AI message, payment)
|
||||
├── start.py # Gunicorn process manager (start/stop/restart/status/debug)
|
||||
├── index.html # Single-page frontend (join screen, chat, modals)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── config.json # Optional config file (alternative to env vars)
|
||||
└── static/
|
||||
├── chat.js # Frontend logic (socket events, PM/AI flows, UI)
|
||||
├── crypto.js # AES-GCM-256 encryption wrapper (PBKDF2 key derivation)
|
||||
├── socket.io.min.js # Socket.IO v4 client library
|
||||
└── style.css # Glassmorphism theme (deep purple / neon magenta)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is loaded with the following precedence: **Environment Variable → `config.json` → Default Value**.
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `SECRET_KEY` | Random UUID hex | Flask session secret |
|
||||
| `JWT_SECRET` | Random UUID hex | HMAC signing key for JWTs |
|
||||
| `ADMIN_PASSWORD` | `admin1234` | Shared moderator password |
|
||||
| `DATABASE_URL` | `sqlite:///sexchat.db` | SQLAlchemy database URI |
|
||||
| `AI_FREE_LIMIT` | `3` | Free AI messages before paywall |
|
||||
| `OLLAMA_URL` | `http://localhost:11434` | Ollama API base URL |
|
||||
| `VIOLET_MODEL` | `sam860/dolphin3-llama3.2:3b` | Ollama model tag for Violet |
|
||||
| `PAYMENT_SECRET` | `change-me-payment-secret` | Webhook validation secret |
|
||||
| `HOST` | `0.0.0.0` | Bind address |
|
||||
| `PORT` | `5000` | Bind port |
|
||||
| `SOCKETIO_MESSAGE_QUEUE` | `None` | Redis URL for multi-process Socket.IO |
|
||||
| `REDIS_URL` | `None` | Fallback for `SOCKETIO_MESSAGE_QUEUE` |
|
||||
|
||||
Create a `config.json` in the project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"SECRET_KEY": "your-secret-key",
|
||||
"JWT_SECRET": "your-jwt-secret",
|
||||
"ADMIN_PASSWORD": "a-strong-moderator-password",
|
||||
"DATABASE_URL": "postgresql://user:pass@localhost/sexchat",
|
||||
"PAYMENT_SECRET": "your-stripe-webhook-secret",
|
||||
"OLLAMA_URL": "http://localhost:11434",
|
||||
"VIOLET_MODEL": "sam860/dolphin3-llama3.2:3b",
|
||||
"AI_FREE_LIMIT": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- pip
|
||||
- Ollama (optional — required only for live AI responses)
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
git clone https://git.computertech.dev/lord3nd3r/aprhodite.git
|
||||
cd aprhodite
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
On first run, `database.py` calls `db.create_all()` and seeds the Violet bot user. No manual migration is needed for a fresh start. For schema changes:
|
||||
|
||||
```bash
|
||||
flask db migrate -m "description"
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
### Ollama (for live AI)
|
||||
|
||||
```bash
|
||||
# Install Ollama (https://ollama.com)
|
||||
ollama pull sam860/dolphin3-llama3.2:3b
|
||||
ollama serve # default :11434
|
||||
```
|
||||
|
||||
If Ollama is unavailable, `routes.py` falls back to canned responses; `app.py` returns a placeholder message.
|
||||
|
||||
---
|
||||
|
||||
## Running the Server
|
||||
|
||||
### Process Manager (recommended for production)
|
||||
|
||||
```bash
|
||||
python start.py start # Start as background daemon
|
||||
python start.py stop # Graceful shutdown
|
||||
python start.py restart # Stop + start
|
||||
python start.py status # Check if running
|
||||
python start.py debug # Foreground with debug logging
|
||||
```
|
||||
|
||||
### Direct Gunicorn
|
||||
|
||||
```bash
|
||||
gunicorn --worker-class eventlet -w 1 --bind 0.0.0.0:5000 start:application
|
||||
```
|
||||
|
||||
### Development (Flask built-in server)
|
||||
|
||||
```python
|
||||
from app import create_app, socketio
|
||||
app = create_app()
|
||||
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Socket Protocol
|
||||
|
||||
### Client → Server Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `join` | `{ mode, username, password?, email?, mod_password? }` | Authenticate and enter the lobby. `mode` is `guest`, `login`, `register`, or `restore`. |
|
||||
| `message` | `{ text }` | Send a message to the lobby. |
|
||||
| `pm_open` | `{ target }` | Request a private message room with `target` user. |
|
||||
| `pm_accept` | `{ room }` | Accept an incoming PM invitation. |
|
||||
| `pm_message` | `{ room, text? }` or `{ room, ciphertext, nonce, transit_key? }` | Send a PM. Include `transit_key` when messaging Violet. |
|
||||
| `ai_message` | `{ ciphertext, nonce, transit_key }` | *Deprecated.* Use `pm_message` to Violet instead. |
|
||||
| `mod_kick` | `{ target }` | Kick a user (admin only). |
|
||||
| `mod_ban` | `{ target }` | Ban a user by username + IP (admin only). |
|
||||
| `mod_kickban` | `{ target }` | Kick and ban simultaneously (admin only). |
|
||||
| `mod_mute` | `{ target }` | Toggle mute on a user (admin only). |
|
||||
| `mod_verify` | `{ target }` | Verify a registered user's account (admin only). |
|
||||
| `user_ignore` | `{ target }` | Add user to ignore list (registered only). |
|
||||
| `user_unignore` | `{ target }` | Remove user from ignore list (registered only). |
|
||||
|
||||
### Server → Client Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `joined` | `{ username, is_admin, is_registered, has_ai_access, ai_messages_used, token?, ignored_list? }` | Successful join confirmation. |
|
||||
| `nicklist` | `{ users: [{ username, is_admin, is_registered, is_verified, is_ai? }] }` | Updated user list. |
|
||||
| `message` | `{ username, text, is_admin, is_registered, ts }` | Lobby message. |
|
||||
| `system` | `{ msg, ts }` | System announcement (joins, parts, mod actions). |
|
||||
| `error` | `{ msg }` | Error message. |
|
||||
| `kicked` | `{ msg }` | Notification that the user has been kicked/banned. |
|
||||
| `pm_invite` | `{ from, room }` | Incoming PM invitation. |
|
||||
| `pm_ready` | `{ with, room }` | PM room is ready (sent to initiator). |
|
||||
| `pm_message` | `{ from, text?, ciphertext?, nonce?, room, ts }` | Private message. |
|
||||
| `violet_typing` | `{ busy: bool, room? }` | Violet AI typing indicator. |
|
||||
| `ai_response` | `{ ciphertext, nonce, ai_messages_used, has_ai_access }` or `{ error }` | AI response (legacy event). |
|
||||
| `ai_unlock` | `{ msg }` | AI access unlocked after payment. |
|
||||
| `ignore_status` | `{ target, ignored: bool }` | Confirmation of ignore/unignore action. |
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
All endpoints are under the `/api` prefix.
|
||||
|
||||
### `POST /api/auth/register`
|
||||
|
||||
Create a new account.
|
||||
|
||||
**Body:** `{ "username": "...", "password": "...", "email": "..." }`
|
||||
**Response:** `201 { "token": "jwt...", "user": { id, username, has_ai_access, ai_messages_used } }`
|
||||
|
||||
### `POST /api/auth/login`
|
||||
|
||||
Authenticate and receive a JWT.
|
||||
|
||||
**Body:** `{ "username": "...", "password": "..." }`
|
||||
**Response:** `200 { "token": "jwt...", "user": { ... } }`
|
||||
|
||||
### `GET /api/pm/history?with=<username>`
|
||||
|
||||
Retrieve encrypted PM history with another user. Requires `Authorization: Bearer <token>`.
|
||||
|
||||
**Response:** `200 { "messages": [{ from_me, ciphertext, nonce, ts }] }`
|
||||
|
||||
### `POST /api/ai/message`
|
||||
|
||||
Send an encrypted message to Violet via REST (alternative to socket).
|
||||
|
||||
**Body:** `{ "ciphertext": "...", "nonce": "...", "transit_key": "..." }`
|
||||
**Response:** `200 { "ciphertext": "...", "nonce": "...", "ai_messages_used": N, "has_ai_access": bool }`
|
||||
|
||||
### `POST /api/payment/success`
|
||||
|
||||
Payment webhook to unlock AI access.
|
||||
|
||||
**Body:** `{ "secret": "..." }`
|
||||
**Response:** `200 { "status": "ok", "has_ai_access": true }`
|
||||
|
||||
---
|
||||
|
||||
## Encryption Design
|
||||
|
||||
### User-to-User PMs
|
||||
|
||||
1. **Key Derivation** — The client derives an AES-GCM-256 key from `(password, username)` using PBKDF2 with 100,000 iterations and SHA-256. The salt is `"sexychat:v1:<lowercase_username>"`.
|
||||
2. **Encryption** — Messages are encrypted client-side with a random 12-byte nonce before being sent over the socket.
|
||||
3. **Storage** — The server stores only the base64-encoded ciphertext and nonce. It never sees the plaintext or key.
|
||||
4. **Decryption** — The recipient decrypts client-side using their own derived key.
|
||||
|
||||
### Violet AI (Transit Encryption)
|
||||
|
||||
1. The client encrypts the message with their derived key and also sends the key (`transit_key`) to the server over HTTPS/WSS.
|
||||
2. The server decrypts the message (transit), passes plaintext to Ollama for inference, then re-encrypts the AI response with the same transit key.
|
||||
3. The transit key is used only in memory and is never stored.
|
||||
|
||||
---
|
||||
|
||||
## Moderation System
|
||||
|
||||
Moderators authenticate by providing the shared `ADMIN_PASSWORD` alongside their normal login credentials. Mod powers include:
|
||||
|
||||
| Action | Effect |
|
||||
|--------|--------|
|
||||
| **Kick** | Disconnects the target. They can rejoin immediately. |
|
||||
| **Ban** | Adds username + IP to in-memory ban lists. Persists until server restart. |
|
||||
| **Kickban** | Kick + ban in one action. |
|
||||
| **Mute / Unmute** | Toggles mute. Muted users cannot send lobby messages. |
|
||||
| **Verify** | Marks a registered account as verified, allowing them to log in. New registrations require moderator verification before the account can be used. |
|
||||
|
||||
---
|
||||
|
||||
## Violet AI Companion
|
||||
|
||||
Violet is an AI persona backed by a local Ollama model. She is configured as a "flirtatious and sophisticated nightclub hostess" via a system prompt.
|
||||
|
||||
- **Model**: Configurable via `VIOLET_MODEL` (default: `sam860/dolphin3-llama3.2:3b`)
|
||||
- **Inference**: Serialised through an eventlet queue — one request at a time to avoid overloading the local GPU/CPU
|
||||
- **Typing indicator**: `violet_typing` events are broadcast while Ollama is processing
|
||||
- **Fallback**: If Ollama fails, a graceful fallback message is returned
|
||||
- **Free tier**: Configurable via `AI_FREE_LIMIT` (default: 3 messages)
|
||||
|
||||
---
|
||||
|
||||
## Payment & Premium Access
|
||||
|
||||
The current payment flow is a **stub** intended to be replaced with a real payment provider (e.g. Stripe):
|
||||
|
||||
1. Client sends the `PAYMENT_SECRET` to `POST /api/payment/success`.
|
||||
2. Server validates with constant-time comparison, flips `user.has_ai_access = True`.
|
||||
3. An `ai_unlock` socket event is pushed to the user's active session.
|
||||
|
||||
**For production**: Replace the secret-comparison logic with `stripe.Webhook.construct_event()` using the raw request body and `Stripe-Signature` header.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### `users`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | Integer PK | Auto-increment |
|
||||
| `username` | String(20) | Unique, indexed |
|
||||
| `password_hash` | String(128) | bcrypt |
|
||||
| `email` | String(255) | Unique, nullable |
|
||||
| `has_ai_access` | Boolean | Premium flag |
|
||||
| `ai_messages_used` | Integer | Free trial counter |
|
||||
| `is_verified` | Boolean | Moderator-verified flag |
|
||||
| `created_at` | DateTime | UTC timestamp |
|
||||
|
||||
### `messages`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | Integer PK | Auto-increment |
|
||||
| `sender_id` | FK → users.id | |
|
||||
| `recipient_id` | FK → users.id | |
|
||||
| `encrypted_content` | Text | Base64 AES-GCM ciphertext |
|
||||
| `nonce` | String(64) | Base64 AES-GCM nonce/IV |
|
||||
| `timestamp` | DateTime | Indexed with sender/recipient |
|
||||
|
||||
### `user_ignores`
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `id` | Integer PK | Auto-increment |
|
||||
| `ignorer_id` | FK → users.id | |
|
||||
| `ignored_id` | FK → users.id | |
|
||||
| `created_at` | DateTime | |
|
||||
|
||||
Unique composite index on `(ignorer_id, ignored_id)`.
|
||||
|
||||
---
|
||||
|
||||
## Known Security Issues & Bugs
|
||||
|
||||
The following issues have been identified during code review. They are listed by severity.
|
||||
|
||||
---
|
||||
|
||||
### P0 — Critical (fix immediately)
|
||||
|
||||
#### 1. JWT Secret Mismatch Between `app.py` and `routes.py`
|
||||
|
||||
**File:** `app.py` line 84, `routes.py` line 30
|
||||
|
||||
`app.py` generates a random `JWT_SECRET` via `uuid.uuid4().hex` on every server restart. `routes.py` independently hardcodes `"change-me-jwt-secret"` as its default. These are two separate values — JWTs issued by the socket layer (`app.py`) will fail validation in the REST API (`routes.py`) and vice versa. **Authentication is broken across the two entry points.**
|
||||
|
||||
**Impact:** Users who log in via sockets cannot use the REST API, and vice versa. In the worst case, if `routes.py`'s default is left in production, its JWT secret is publicly known from the source code, allowing anyone to forge tokens.
|
||||
|
||||
**Fix:** Use a single shared `JWT_SECRET` loaded once from configuration. Remove the duplicate constant in `routes.py` and import from `app.py` or a shared config module.
|
||||
|
||||
---
|
||||
|
||||
#### 2. Unauthorised PM Room Join (`pm_accept`)
|
||||
|
||||
**File:** `app.py` — `on_pm_accept` handler
|
||||
|
||||
```python
|
||||
@socketio.on("pm_accept")
|
||||
def on_pm_accept(data):
|
||||
join_room(data.get("room")) # no validation
|
||||
```
|
||||
|
||||
Any client can emit `pm_accept` with an arbitrary room name (e.g. `pm:alice:bob`) and silently join that room, receiving all future messages. There is no check that the user was actually invited to this room.
|
||||
|
||||
**Impact:** Complete compromise of private messaging confidentiality (for plaintext/guest PMs). For encrypted PMs, the attacker receives ciphertext but not keys — however, it still leaks metadata (who is talking, when, message sizes).
|
||||
|
||||
**Fix:** Maintain a server-side mapping of pending invitations. Only allow `pm_accept` if the user's SID has a pending invite for that specific room.
|
||||
|
||||
---
|
||||
|
||||
#### 3. Client-Exploitable Payment Endpoint
|
||||
|
||||
**Files:** `routes.py` — `payment_success`, `chat.js` lines 560–574
|
||||
|
||||
The payment "verification" consists of the client sending a secret string to the server. The default `PAYMENT_SECRET` is `"change-me-payment-secret"` (known from source). Even if changed, the client-side code in `chat.js` hardcodes the secret and sends it directly:
|
||||
|
||||
```javascript
|
||||
const secret = "change-me-payment-webhook-secret";
|
||||
```
|
||||
|
||||
Any user with a valid JWT can call `POST /api/payment/success` with the secret and unlock premium AI access for free.
|
||||
|
||||
**Impact:** Complete bypass of the payment system. Any registered user gets unlimited AI access without paying.
|
||||
|
||||
**Fix:** Replace with a server-side Stripe/payment-provider webhook. The payment confirmation must originate from the payment provider's servers, not from the client.
|
||||
|
||||
---
|
||||
|
||||
#### 4. PM Invite Broadcast When PMing Violet
|
||||
|
||||
**File:** `app.py` — `on_pm_open` handler, lines 607–612
|
||||
|
||||
When a user opens a PM with Violet, `target_sid` is `None` (Violet has no socket connection). The code then executes:
|
||||
|
||||
```python
|
||||
socketio.emit("pm_invite", {"from": user["username"], "room": room}, to=None)
|
||||
```
|
||||
|
||||
Emitting `to=None` broadcasts the event to **all connected clients**, informing everyone that the user is starting a private conversation with Violet.
|
||||
|
||||
**Impact:** Privacy leak — all connected users learn who is chatting with Violet.
|
||||
|
||||
**Fix:** Skip the `pm_invite` emit when the target is Violet. The initiator already receives `pm_ready`; no invite is needed for a bot.
|
||||
|
||||
---
|
||||
|
||||
### P1 — High Severity
|
||||
|
||||
#### 5. E2E Encryption Is Fundamentally Broken for User-to-User PMs
|
||||
|
||||
**File:** `static/crypto.js` — `deriveKey`
|
||||
|
||||
Keys are derived from `(password, username)` using PBKDF2. When User A sends a PM to User B:
|
||||
- User A encrypts with a key derived from *A's password and A's username*.
|
||||
- User B attempts to decrypt with a key derived from *B's password and B's username*.
|
||||
- These are entirely different keys. **Decryption will always fail.**
|
||||
|
||||
The only scenario where this works is when a user decrypts their own messages (e.g. PM history reload), since the key matches. But real-time cross-user PMs cannot be decrypted.
|
||||
|
||||
**Impact:** User-to-user encrypted PMs are non-functional. Users will see decryption errors for every received message.
|
||||
|
||||
**Fix:** Implement a proper key exchange protocol (e.g. ECDH / X25519) where both parties derive a shared secret, or use a server-mediated key-wrapping scheme.
|
||||
|
||||
---
|
||||
|
||||
#### 6. Null DOM Element References Crash the Frontend
|
||||
|
||||
**File:** `static/chat.js`
|
||||
|
||||
- Line 547: `$("tab-ai-violet")` — references an element ID that does not exist in `index.html`.
|
||||
- Line 44: `$("violet-trial-badge")` — also missing from the HTML.
|
||||
|
||||
These return `null`, and any property access (e.g. `.onclick`, `.classList`) will throw `TypeError: Cannot read properties of null`.
|
||||
|
||||
**Impact:** JavaScript execution halts at these points, potentially breaking logout, tab switching, or the entire chat UI on load.
|
||||
|
||||
**Fix:** Add the missing elements to `index.html`, or add null guards before accessing these elements.
|
||||
|
||||
---
|
||||
|
||||
#### 7. CORS Wildcard on WebSocket
|
||||
|
||||
**File:** `app.py` line 381
|
||||
|
||||
```python
|
||||
socketio.init_app(app, cors_allowed_origins="*", ...)
|
||||
```
|
||||
|
||||
This allows any website on the internet to open WebSocket connections to the server. A malicious page could connect on behalf of a visiting user if cookies/tokens are available.
|
||||
|
||||
**Impact:** Cross-site WebSocket hijacking. An attacker's page could impersonate users, send messages, or eavesdrop on the lobby.
|
||||
|
||||
**Fix:** Restrict `cors_allowed_origins` to the actual domain(s) serving the frontend.
|
||||
|
||||
---
|
||||
|
||||
### P2 — Medium Severity
|
||||
|
||||
#### 8. Massive Code Duplication Between `app.py` and `routes.py`
|
||||
|
||||
Both files independently define their own copies of:
|
||||
- `_aesgcm_encrypt` / `_aesgcm_decrypt`
|
||||
- `_issue_jwt` / `_verify_jwt`
|
||||
- `_save_pm` / `_persist_message`
|
||||
- Constants: `AI_FREE_LIMIT`, `AI_BOT_NAME`, `JWT_SECRET`
|
||||
|
||||
They have already diverged — `routes.py` uses mock AI responses (`random.choice(AI_RESPONSES)`) while `app.py` calls Ollama. The JWT secrets are different (see issue #1).
|
||||
|
||||
**Impact:** Bugs fixed in one file won't be fixed in the other. Behaviour differs depending on whether the user interacts via sockets or REST. Maintenance burden grows over time.
|
||||
|
||||
**Fix:** Extract shared utilities (crypto, JWT, DB helpers, constants) into a common module (e.g. `utils.py` or `config.py`). Import from there in both `app.py` and `routes.py`.
|
||||
|
||||
---
|
||||
|
||||
#### 9. Context Menu Breaks After First Use
|
||||
|
||||
**File:** `static/chat.js` — `showContextMenu` function, lines 453–455
|
||||
|
||||
```javascript
|
||||
const newMenu = contextMenu.cloneNode(true);
|
||||
contextMenu.replaceWith(newMenu);
|
||||
```
|
||||
|
||||
The function clones and replaces the context menu DOM node to clear event listeners. However, the module-level `contextMenu` variable still references the old (now-removed) node. On the second right-click, the code operates on a detached element.
|
||||
|
||||
**Impact:** Context menu stops working after the first use. Users can only PM/ignore/mod-action once per page load.
|
||||
|
||||
**Fix:** Update the variable reference after replacement, or use `removeEventListener` / event delegation instead of cloning.
|
||||
|
||||
---
|
||||
|
||||
#### 10. All Runtime State Is In-Memory Only
|
||||
|
||||
**File:** `app.py` lines 113–118
|
||||
|
||||
`connected_users`, `banned_usernames`, `banned_ips`, and `muted_users` are plain Python dicts/sets. All bans, mutes, and IP blocks are lost on every server restart.
|
||||
|
||||
**Impact:** Banned users can return simply by waiting for a server restart. Moderator actions have no lasting effect.
|
||||
|
||||
**Fix:** Persist bans and mutes to the database. Load them into memory on startup.
|
||||
|
||||
---
|
||||
|
||||
### P3 — Low Severity
|
||||
|
||||
#### 11. Deprecated `datetime.utcnow()` Usage
|
||||
|
||||
**File:** `models.py` — all `default=datetime.utcnow` column definitions
|
||||
|
||||
`datetime.utcnow()` is deprecated as of Python 3.12. It returns a naive datetime with no timezone info, which can cause subtle bugs with timezone-aware code.
|
||||
|
||||
**Impact:** Deprecation warnings on Python 3.12+. Potential timezone-related bugs if the codebase later mixes aware and naive datetimes.
|
||||
|
||||
**Fix:** Use `datetime.now(datetime.timezone.utc)` or `func.now()` (SQLAlchemy server default) instead.
|
||||
|
||||
---
|
||||
|
||||
#### 12. Default Admin Password
|
||||
|
||||
**File:** `app.py` line 85
|
||||
|
||||
```python
|
||||
ADMIN_PASSWORD = _get_conf("ADMIN_PASSWORD", "admin1234")
|
||||
```
|
||||
|
||||
If the operator does not set a custom admin password, anyone who reads the source code or guesses `admin1234` gains full moderator access (kick, ban, mute, verify users).
|
||||
|
||||
**Impact:** Unauthorised moderator access in default-configured deployments.
|
||||
|
||||
**Fix:** Require `ADMIN_PASSWORD` to be explicitly set in config. Refuse to start (or disable mod features) if it's left at the default value.
|
||||
Loading…
Reference in New Issue