🛡

Security Audit Report

Tudebot Clash-of-Clans Bot
2026-03-22
1
Critical
4
High
7
Medium
5
Low
5
Info
5
Fixed

Executive Summary

The Clash of Clans bot has received important security fixes (CRIT-001 through CRIT-003) addressing hardcoded session secrets, ADB command injection, and config path traversal. The remaining attack surface contains significant weaknesses: wildcard CORS combined with absent CSRF tokens allows cross-site request forgery against all state-changing endpoints, and unsanitised strategy file names enable arbitrary JSON writes anywhere the process can write. Together these form a chained RCE path documented as CRIT-001.

Findings

22 findings
CRIT-001 Critical Fixed Config save + strategy path traversal = remote code execution chain web/app.py, strategy_recorder.py
CategoryRemote Code Execution (Chained)
CWECWE-22, CWE-78, CWE-94, CWE-20
OWASPA03:2021, A01:2021

Description

Multiple overlapping paths combine HIGH-001 (unrestricted config write), MED-003 (strategy path traversal write), and MED-004 (strategy path traversal read) to achieve arbitrary file writes and potentially code execution. These paths are individually serious but become critical when chained.

Exploit Path A — Template Overwrite

curl -b "session=<VALID>" -X POST http://localhost:5000/api/strategy/record/start
curl -b "session=<VALID>" -X POST http://localhost:5000/api/strategy/tap \
     -H "Content-Type: application/json" -d '{"x":1,"y":1}'
curl -b "session=<VALID>" -X POST http://localhost:5000/api/strategy/record/stop \
     -H "Content-Type: application/json" \
     -d '{"name":"../../web/templates/index"}'
# Result: web/templates/index.json overwritten — dashboard DoS

Exploit Path B — Log Redirection + Replay

POST /api/config  body: {"config": "logging:\n  file: ../strategies/payload.json\n..."}
# Trigger log messages with "tap_down" JSON structure
POST /api/strategy/replay  body: {"name": "payload"}
# Result: ADB taps at attacker-specified coordinates

Remediation

import re, os
from pathlib import Path

def _safe_strategy_name(name: str) -> str:
    clean = re.sub(r'[^a-zA-Z0-9_\-]', '_', str(name))[:64]
    return clean or "default"

def _safe_strategy_path(name: str) -> Path:
    base = Path("strategies").resolve()
    candidate = (base / f"{_safe_strategy_name(name)}.json").resolve()
    if not str(candidate).startswith(str(base) + os.sep):
        raise ValueError("Path traversal attempt")
    return candidate

# Also: add CSRF protection, restrict logging.file to logs/
FIXED (2026-03-22): Strategy names sanitized via _safe_strategy_name() with strict regex [a-zA-Z0-9_\-] and max 64 chars. _safe_strategy_path() validates resolved path stays within strategies/ directory. Both save and replay use these safe helpers. Config save now validates logging.file is restricted to logs/ directory.
HIGH-001 High Fixed Config save endpoint allows unrestricted YAML write web/app.py:237-260
CategoryConfiguration Tampering
CWECWE-15, CWE-20
OWASPA05:2021

Description

/api/config POST accepts arbitrary YAML after only a syntax check. An attacker can set safety.dry_run=false, attack.min_loot to 0, device.serial to any device, or logging.file to any writable path. Combined with MED-001 (CSRF), this is exploitable cross-site.

Evidence

new_config_text = request.json.get("config", "")
yaml.safe_load(new_config_text)  # syntax only — no schema validation
with open(config_path, "w") as f:
    f.write(new_config_text)     # unrestricted write

Remediation

Add a schema validator checking allowed keys, value types, and ranges. Restrict logging.file to within logs/. Add CSRF protection.

FIXED (2026-03-22): Added _validate_config_schema() that validates parsed YAML before writing. logging.file restricted to logs/ directory. safety.dry_run validated as bool, safety.max_runtime_hours as 0-168, safety.max_attacks as 0-1000, device.serial as alphanumeric with max 64 chars. Invalid configs rejected with 400.
HIGH-002 High Fixed WebSocket logs broadcast to all connected clients web/app.py:122-133
CategoryInformation Disclosure
CWECWE-200, CWE-284

Description

WebLogHandler emits all log records via global socketio.emit(), not per-session. In debug mode this streams every tap coordinate, template match result, OCR loot value, and ADB stderr to all connected WebSocket clients.

Evidence

socketio.emit("log", {"message": msg})  # global broadcast

Remediation

Use per-session rooms. Never emit DEBUG-level messages to the web UI.

FIXED (2026-03-22): WebLogHandler now filters out DEBUG-level records. All socketio.emit() calls use per-session rooms via the to= parameter. Authenticated clients join _authenticated_rooms on connect and are removed on disconnect. Background emitters iterate over authenticated rooms only.
HIGH-003 High Fixed No session expiry — stolen cookie provides permanent access web/app.py:47-113
CategoryAuthentication
CWECWE-613
OWASPA07:2021

Description

login_required checks only session.get("logged_in") — a boolean with no timestamp, IP binding, or rotation. No session lifetime is configured. A stolen session cookie grants unrestricted access indefinitely.

Remediation

from datetime import timedelta
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=8)
app.config["SESSION_PERMANENT"] = True

# On login:
session.clear()
session["logged_in"] = True
session["login_time"] = time.time()
FIXED (2026-03-22): Added SESSION_PERMANENT = True and PERMANENT_SESSION_LIFETIME = timedelta(hours=8). Session is cleared and rotated on login to prevent fixation. session.permanent = True set explicitly per login.
HIGH-004 High Fixed adb shell monkey — Android shell injection if package sourced from config bot/core.py:323-324
CategoryCommand Injection
CWECWE-78

Description

_relog() uses "adb shell monkey -p <package>". Arguments after "shell" are assembled into a command string on the Android device. If the package name is ever sourced from config (via HIGH-001), shell metacharacters would inject arbitrary Android shell commands.

Evidence

self.adb._run("shell", "monkey", "-p", self._coc_package, "-c",
               "android.intent.category.LAUNCHER", "1")

Remediation

# Replace monkey with am start:
self.adb._run("shell", "am", "start",
              "-a", "android.intent.action.MAIN",
              "-c", "android.intent.category.LAUNCHER",
              "-n", f"{self._coc_package}/.MainActivity")
FIXED (2026-03-22): Replaced adb shell monkey with adb shell am start in _relog(). Uses "am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n <package>/com.supercell.titan.GameApp" which only launches a specific activity without monkey's event injection capabilities.
MED-001 Medium No CSRF protection on any state-changing API endpoint web/app.py:174-359
CategoryCSRF
CWECWE-352
OWASPA01:2021

Description

None of the POST endpoints implement CSRF protection. Combined with wildcard CORS (MED-002), any webpage visited by an authenticated operator can start/stop the bot, save arbitrary config, or trigger attacks.

Evidence

// PoC from attacker-controlled page:
fetch("http://localhost:5000/api/start", {
    method: "POST",
    body: JSON.stringify({mode: "attack"}),
    credentials: "include",
    headers: {"Content-Type": "application/json"}
});

Remediation

from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
# In JS: headers: {"X-CSRFToken": getCsrfToken()}
MED-002 Medium Wildcard CORS policy on Flask-SocketIO web/app.py:42
CategoryCORS Misconfiguration
CWECWE-942
OWASPA05:2021

Description

SocketIO is initialised with cors_allowed_origins="*", permitting any origin to connect and issue requests.

Evidence

socketio = SocketIO(app, cors_allowed_origins="*")

Remediation

socketio = SocketIO(app, cors_allowed_origins=[
    "http://localhost:5000",
    "http://127.0.0.1:5000"
])
MED-003 Medium Path traversal in strategy file name — arbitrary JSON write strategy_recorder.py:45-67
CategoryPath Traversal
CWECWE-22
OWASPA03:2021

Description

Strategy name from /api/strategy/record/stop is used directly in os.path.join() with no sanitisation, allowing directory traversal to overwrite any writable file.

Evidence

filepath = os.path.join(STRATEGIES_DIR, f"{name}.json")
# Payload: {"name": "../../web/templates/index"}

Remediation

Regex-sanitize name with [a-zA-Z0-9_\-] and resolve-path check within STRATEGIES_DIR.

MED-004 Medium Strategy replay accepts unsanitised file name strategy_recorder.py:73-109
CategoryPath Traversal
CWECWE-22, CWE-294

Description

/api/strategy/replay constructs a file path from client-supplied name with unsanitised join. An attacker can point replay at any JSON file on the system to execute its events as ADB taps.

Remediation

Apply the same _safe_strategy_path() function (see MED-003) to both stop_recording() and replay().

MED-005 Medium CoC API proxy — insufficient path validation (SSRF risk) web/app.py:398-443
CategorySSRF
CWECWE-918
OWASPA10:2021

Description

The /api/coc/proxy endpoint validates only that path starts with "/". No allowlist of valid API path prefixes is enforced.

Remediation

ALLOWED_PREFIXES = ("/clans/", "/players/", "/leagues/", "/locations/",
                    "/labels/", "/goldpass/", "/warleagues/")
if not any(path.startswith(p) for p in ALLOWED_PREFIXES):
    return jsonify({"error": "Path not allowed"}), 400
MED-006 Medium CoC API token stored in browser localStorage web/templates/index.html:753
CategorySensitive Data Exposure
CWECWE-922
OWASPA02:2021

Description

The CoC API key is persisted to localStorage, readable by any same-origin JavaScript. An XSS payload (MED-007) can exfiltrate it.

Remediation

Store the API key in the server-side Flask session. If client storage is unavoidable, use sessionStorage instead of localStorage.

MED-007 Medium Potential XSS — unsanitised troop name in innerHTML web/templates/index.html:388
CategoryCross-Site Scripting
CWECWE-79
OWASPA03:2021

Description

Troop names from SocketIO stats are inserted into innerHTML without escaping. If a troop name contains HTML/JS (e.g. from a config value set via HIGH-001), this results in stored XSS.

Evidence

return `<tr><td>${timeStr}</td><td>${h.troop}</td></tr>`;
// No escaping applied

Remediation

// escH() already exists — apply it:
return `<tr><td>${escH(timeStr)}</td><td>${escH(h.troop)}</td></tr>`;
LOW-001 Low Session cookie missing Secure and SameSite attributes web/app.py
CategorySession Management
CWECWE-614

Remediation

app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
LOW-002 Low Logout route accepts GET requests — CSRF logout web/app.py:110-113
CategoryCSRF
CWECWE-352

Description

An <img src="/logout"> on any page visited while authenticated silently logs the user out.

Remediation

@app.route("/logout", methods=["POST"])
@login_required
def logout():
    session.clear()
    return redirect(url_for("login"))
LOW-003 Low Unpinned dependency versions — supply chain risk requirements.txt
CategorySupply Chain
CWECWE-1395
OWASPA06:2021

Description

All dependencies use >= constraints. A supply-chain compromise could silently execute malicious code.

Remediation

pip-compile --generate-hashes requirements.in
# Or: flask~=3.0.3, flask-socketio~=5.3.6
LOW-004 Low Loot values and device resolution logged at INFO level bot/actions/attacker.py, bot/core.py
CategoryInformation Disclosure
CWECWE-532

Remediation

Downgrade loot values and device resolution logging to DEBUG. Restrict log file permissions: chmod 600 logs/bot.log.

LOW-005 Low allow_unsafe_werkzeug=True — development server in production web/app.py:450
CategoryConfiguration

Remediation

gunicorn --worker-class eventlet -w 1 "web.app:app"
INFO-001 Info users.json tracked by git history users.json, .gitignore
CategoryCredentials / VCS
CWECWE-312

Description

users.json was committed in a past revision containing a username and pbkdf2:sha256 hash, subject to offline dictionary attack.

Remediation

git filter-repo --path users.json --invert-paths
# Rotate the account password immediately
INFO-002 Info Ephemeral session secret — sessions reset on every restart web/app.py:32-41

Description

When COC_BOT_SECRET is unset, a random secret is generated. Sessions are lost on every restart.

Remediation

Document COC_BOT_SECRET as required. Consider sys.exit(1) if unset.

INFO-003 Info Unencrypted ADB communication channel bot/adb_controller.py
CWECWE-319

Remediation

Prefer USB-only mode. For TCP ADB, tunnel via SSH.

INFO-004 Info No rate limiting on login endpoint web/app.py:75-88
CWECWE-307

Remediation

from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)

@limiter.limit("10 per 10 minutes")
@app.route("/login", methods=["GET", "POST"])
def login(): ...
INFO-005 Info Strategy files with device tap coordinates committed to git strategies/*.json

Description

Three strategy files with real device resolution (1080x2340) and 35+ tap event coordinates are committed, revealing automation patterns.

Remediation

Add strategies/ to .gitignore.

Remediation Priority

#IDTitleSeverityEffortFix
1CRIT-001 Strategy path traversal + config write RCE chain Critical 2 hrs Sanitize strategy names; add CSRF; schema-validate config
2MED-001 No CSRF protection Medium 3 hrs Flask-WTF CSRFProtect + JS token header
3MED-003 Path traversal in strategy save Medium 30 min Regex-sanitize name; resolve-path check
4HIGH-001 Unrestricted YAML config write High 4 hrs YAML schema validator; restrict logging.file
5MED-002 Wildcard CORS Medium 15 min Restrict to localhost origins

Fix Assessment

IDIssueStatusNotes
CRIT-001 (orig) Hardcoded SECRET_KEY FIXED Random ephemeral key generation is correct. No regression.
CRIT-001 (new) Strategy path traversal + config write RCE chain FIXED Strategy names sanitized with strict [a-zA-Z0-9_\-] regex and resolved-path validation in both save and replay. Config schema validation closes the log-redirect exploit path.
CRIT-002 ADB command injection via coordinates FIXED int() casting + 0–4096 range validation is sound. No bypass found.
CRIT-003 Path traversal in config read/write FIXED Config path validated against project root. Strategy path traversal (previously partial gap) now also closed by _safe_strategy_path().
HIGH-001 Unrestricted YAML config write FIXED YAML schema validator added — checks allowed keys, value types, and ranges. logging.file restricted to the logs/ directory.
HIGH-002 WebSocket logs broadcast to all clients FIXED Per-session rooms implemented. Logs emitted only to authenticated rooms. DEBUG-level messages filtered out of the web UI.
HIGH-003 No session expiry FIXED Sessions expire after 8 hours via PERMANENT_SESSION_LIFETIME. Session is cleared on login to prevent session fixation.
HIGH-004 adb shell monkey — Android shell injection FIXED Replaced adb shell monkey with adb shell am start to eliminate shell injection vector.