# Bug Fixes — Non-Feature Realism Issues
> These are fingerprint leaks and broken protocol handlers that don't need new
> interaction design — just targeted fixes. All severity High or above from REALISM_AUDIT.md.
---
## 1. HTTP — Werkzeug header leak (High)
### Problem
Every response has two `Server:` headers:
```
Server: Werkzeug/3.1.3 Python/3.11.2
Server: Apache/2.4.54 (Debian)
```
nmap correctly IDs Apache from the second header, but any attacker that does
`curl -I` or runs Burp sees the Werkzeug leak immediately. Port 6443 (K8s) also
leaks Werkzeug in every response.
### Fix — WSGI middleware to strip/replace the header
In `templates/http/server.py` (Flask app), add a `@app.after_request` hook:
```python
@app.after_request
def _fix_server_header(response):
response.headers["Server"] = os.environ.get("HTTP_SERVER_HEADER", "Apache/2.4.54 (Debian)")
return response
```
Flask sets `Server: Werkzeug/...` by default. The `after_request` hook runs after
Werkzeug's own header injection, so it overwrites it.
Same fix applies to the K8s server if it's also Flask-based.
### Fix — Apache 403 page body
Current response body: `
403 Forbidden
`
Replace with the actual Apache 2.4 default 403 page:
```html
403 Forbidden
Forbidden
You don't have permission to access this resource.
Apache/2.4.54 (Debian) Server at {hostname} Port 80
```
Env var `HTTP_SERVER_HEADER` and `NODE_NAME` fill the address line.
### Env vars
| Var | Default |
|-----|---------|
| `HTTP_SERVER_HEADER` | `Apache/2.4.54 (Debian)` |
---
## 2. FTP — Twisted banner (High)
### Problem
```
220 Twisted 25.5.0 FTP Server
```
This is Twisted's built-in FTP server banner. Immediately identifies the framework.
### Fix
Override the banner. The Twisted FTP server class has a `factory.welcomeMessage` or
the protocol's `sendLine()` for the greeting. Simplest fix: subclass the protocol
and override `lineReceived` to intercept the `220` line before it goes out, OR
use a `_FTPFactory` subclass that sets `welcomeMessage`:
```python
from twisted.protocols.ftp import FTPFactory, FTPAnonymousShell
from twisted.internet import reactor
import os
NODE_NAME = os.environ.get("NODE_NAME", "ftpserver")
BANNER = os.environ.get("FTP_BANNER", f"220 (vsFTPd 3.0.3)")
factory = FTPFactory(portal)
factory.welcomeMessage = BANNER # overrides the Twisted default
```
If `FTPFactory.welcomeMessage` is not directly settable, patch it at class level:
```python
FTPFactory.welcomeMessage = BANNER
```
### Anonymous login + fake directory
The current server rejects everything after login. Fix:
- Use `FTPAnonymousShell` pointed at a `MemoryFilesystem` with fake entries:
```
/
├── backup.tar.gz (0 bytes, but listable)
├── db_dump.sql (0 bytes)
├── config.ini (0 bytes)
└── credentials.txt (0 bytes)
```
- `RETR` any file → return 1–3 lines of plausible fake content, then close.
- Log every `RETR` with filename and client IP.
### Env vars
| Var | Default |
|-----|---------|
| `FTP_BANNER` | `220 (vsFTPd 3.0.3)` |
---
## 3. MSSQL — Silent on TDS pre-login (High)
### Problem
No response to standard TDS pre-login packets. Connection is dropped silently.
nmap barely recognizes port 1433 (`ms-sql-s?`).
### Diagnosis
The nmap fingerprint shows `\x04\x01\x00\x2b...` which is a valid TDS 7.x pre-login
response fragment. So the server is sending _something_ — but it may be truncated or
malformed enough that nmap can't complete its probe.
Check `templates/mssql/server.py`: look for the raw bytes being sent in response to
`\x12\x01` (TDS pre-login type). Common bugs:
- Wrong packet length field (bytes 2-3 of TDS header)
- Missing `\xff` terminator on the pre-login option list
- Status byte 0x01 instead of 0x00 in the TDS header (signaling last packet)
### Correct TDS 7.x pre-login response structure
```
Byte 0: 0x04 (packet type: tabular result)
Byte 1: 0x01 (status: last packet)
Bytes 2-3: 0x00 0x2b (total length including header = 43)
Bytes 4-5: 0x00 0x00 (SPID)
Byte 6: 0x01 (packet ID)
Byte 7: 0x00 (window)
--- TDS pre-login payload ---
[VERSION] option: type=0x00, offset=0x001a, length=0x0006
[ENCRYPTION] option: type=0x01, offset=0x0020, length=0x0001
[INSTOPT] option: type=0x02, offset=0x0021, length=0x0001
[THREADID] option: type=0x03, offset=0x0022, length=0x0004
[MARS] option: type=0x04, offset=0x0026, length=0x0001
Terminator: 0xff
VERSION: 0x0e 0x00 0x07 0xd0 0x00 0x00 (14.0.2000 = SQL Server 2017)
ENCRYPTION: 0x02 (ENCRYPT_NOT_SUP)
INSTOPT: 0x00
THREADID: 0x00 0x00 0x00 0x00
MARS: 0x00
```
Verify the current implementation's bytes match this exactly. Fix the length field if off.
---
## 4. MongoDB — Silent on OP_MSG (High)
### Problem
No response to `OP_MSG isMaster` command. nmap shows `mongod?` (partial recognition).
### Diagnosis
MongoDB wire protocol since 3.6 uses `OP_MSG` (opcode 2013). Older clients use
`OP_QUERY` (opcode 2004) against `admin.$cmd`. Check which one `templates/mongodb/server.py`
handles, and whether the response's `responseTo` field matches the request's `requestID`.
Common bugs:
- Handling `OP_QUERY` but not `OP_MSG`
- Wrong `responseTo` in the response header (must echo the request's requestID)
- Missing `flagBits` field in OP_MSG response (must be 0x00000000)
### Correct OP_MSG `hello` response
```python
import struct, bson
def _op_msg_hello_response(request_id: int) -> bytes:
doc = {
"ismaster": True,
"maxBsonObjectSize": 16777216,
"maxMessageSizeBytes": 48000000,
"maxWriteBatchSize": 100000,
"localTime": {"$date": int(time.time() * 1000)},
"logicalSessionTimeoutMinutes": 30,
"connectionId": 1,
"minWireVersion": 0,
"maxWireVersion": 17,
"readOnly": False,
"ok": 1.0,
}
payload = b"\x00" + bson.encode(doc) # section type 0 = body
flag_bits = struct.pack("