From 840a0e3432cfc4003636288722e2868804bd9242 Mon Sep 17 00:00:00 2001 From: Pathompum Jirakarnpaisan <107536914+Saannddy@users.noreply.github.com> Date: Mon, 4 May 2026 22:30:42 +0700 Subject: [PATCH] feat: implement secure, in-memory cheat mode with authorized toggle endpoint to bypass test execution --- .env.example | 7 +++ src/api/__init__.py | 2 + src/api/routes/cheat_routes.py | 43 +++++++++++++ src/core/__init__.py | 1 + src/core/cheat.py | 108 +++++++++++++++++++++++++++++++++ src/core/executor.py | 5 ++ 6 files changed, 166 insertions(+) create mode 100644 src/api/routes/cheat_routes.py create mode 100644 src/core/cheat.py diff --git a/.env.example b/.env.example index b8b31e6..f6a5c5d 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,10 @@ MAX_MEMORY_MB=512 MAX_FILE_SIZE_MB=1 MAX_OPEN_FILES=64 MAX_RUN_TIME=5 + +# Cheat Mode +# CHEAT_CODE must be the SHA-256 hex digest of your secret passphrase. +# The raw secret is NEVER stored here — only its hash. +# To generate: python3 -c "import hashlib; print(hashlib.sha256(b'YOUR_SECRET').hexdigest())" +# State is in-memory only and resets on server restart (safe by design). +CHEAT_CODE= diff --git a/src/api/__init__.py b/src/api/__init__.py index db5451f..3c6101c 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -5,6 +5,7 @@ from .routes.riddle_routes import riddle_bp from .routes.execution_routes import execution_bp from .routes.chunk_routes import chunk_bp +from .routes.cheat_routes import cheat_bp api_bp = Blueprint('api', __name__) @@ -15,3 +16,4 @@ api_bp.register_blueprint(riddle_bp, url_prefix='/riddle') api_bp.register_blueprint(chunk_bp, url_prefix='/chunk') api_bp.register_blueprint(execution_bp) # execution handles its own prefixes (/code, /run) +api_bp.register_blueprint(cheat_bp) # cheat-flip at root level diff --git a/src/api/routes/cheat_routes.py b/src/api/routes/cheat_routes.py new file mode 100644 index 0000000..d339961 --- /dev/null +++ b/src/api/routes/cheat_routes.py @@ -0,0 +1,43 @@ +from flask import Blueprint, request, jsonify +from core.cheat import toggle_cheat_mode + +cheat_bp = Blueprint("cheat", __name__) + + +@cheat_bp.post("/cheat-flip") +def cheat_flip(): + """ + Toggle cheat mode on/off. + + Body (JSON): + { "cheat-code": "" } + + The server compares SHA-256(cheat-code) against the stored CHEAT_CODE hash + using a constant-time comparison, so brute-forcing via timing is not possible. + + Returns 200 with current cheat_mode state on success. + Returns 400 for malformed requests, 401 for wrong code, 500 if unconfigured. + """ + if not request.is_json: + return jsonify(status="error", message="Request body must be JSON"), 400 + + data = request.get_json(silent=True) or {} + cheat_code = data.get("cheat-code") + + if not cheat_code: + return jsonify(status="error", message="Missing 'cheat-code' in request body"), 400 + + result = toggle_cheat_mode(cheat_code) + + if not result["success"]: + if result["cheat_mode"] is None: + # Server-side misconfiguration + return jsonify(status="error", message=result["message"]), 500 + # Wrong code — do NOT reveal whether mode is on or off + return jsonify(status="error", message=result["message"]), 401 + + return jsonify( + status="success", + message=result["message"], + cheat_mode=result["cheat_mode"], + ), 200 diff --git a/src/core/__init__.py b/src/core/__init__.py index a242448..12a5add 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1,2 +1,3 @@ from .executor import execute_code, execute_custom_code from .security.sanitizer import sanitize_code +from .cheat import toggle_cheat_mode, is_cheat_mode, make_all_passed_result diff --git a/src/core/cheat.py b/src/core/cheat.py new file mode 100644 index 0000000..f8be5d8 --- /dev/null +++ b/src/core/cheat.py @@ -0,0 +1,108 @@ +""" +Cheat mode utilities — in-memory state, security-hardened. + +CHEAT_MODE is held purely in memory (module-level flag) and resets on every +server restart. This is intentional: cheat mode should not silently persist +across deployments or container restarts. + +CHEAT_CODE env var holds a SHA-256 hex digest of the secret passphrase. +The raw secret is NEVER stored anywhere — only its hash is in .env. + +Setup: + python3 -c "import hashlib; print(hashlib.sha256(b'YOUR_SECRET').hexdigest())" + # Set CHEAT_CODE= in .env + +Toggling: + POST /cheat-flip { "cheat-code": "YOUR_SECRET" } +""" +import os +import hmac +import hashlib +import threading + +# --------------------------------------------------------------------------- +# In-memory state — process-local, resets on restart +# --------------------------------------------------------------------------- + +# Initialize from CHEAT_MODE env var so you can pre-enable it if needed, +# but this is purely optional. Default is always False (safe). +_cheat_mode: bool = os.getenv("CHEAT_MODE", "false").lower() == "true" +_lock = threading.Lock() + + +# --------------------------------------------------------------------------- +# Security helpers +# --------------------------------------------------------------------------- + +def _hash_secret(raw: str) -> str: + """Return the SHA-256 hex digest of a raw passphrase.""" + return hashlib.sha256(raw.encode()).hexdigest() + + +def _constant_time_verify(raw_input: str, stored_hash: str) -> bool: + """ + Timing-safe comparison: hash the input, then compare fixed-length digests. + Prevents timing-based brute-force attacks. + """ + input_hash = _hash_secret(raw_input) + return hmac.compare_digest(input_hash, stored_hash) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def is_cheat_mode() -> bool: + """Return current cheat mode state (thread-safe read).""" + with _lock: + return _cheat_mode + + +def toggle_cheat_mode(raw_cheat_code: str) -> dict: + """ + Validate raw_cheat_code against the stored SHA-256 hash, then flip the + in-memory cheat mode flag. No disk writes — fast and race-condition-safe. + + Returns: { success: bool, message: str, cheat_mode: bool | None } + """ + global _cheat_mode + + stored_hash = os.getenv("CHEAT_CODE") + if not stored_hash: + return { + "success": False, + "message": "Cheat mode is not configured on this server.", + "cheat_mode": None, + } + + if not _constant_time_verify(raw_cheat_code, stored_hash): + return { + "success": False, + "message": "Invalid cheat code.", + "cheat_mode": None, + } + + with _lock: + _cheat_mode = not _cheat_mode + new_mode = _cheat_mode + + return { + "success": True, + "message": f"Cheat mode {'enabled' if new_mode else 'disabled'}.", + "cheat_mode": new_mode, + } + + +def make_all_passed_result(tests: list) -> dict: + """Build a fake 'all passed' execution result for the given test list.""" + results = [ + { + "case": int(t.get("test_number", i + 1)), + "status": "passed", + "msg": "Test passed.", + "stdout": t.get("expected_output", "").strip(), + "stderr": "", + } + for i, t in enumerate(tests) + ] + return {"status": "correct", "msg": "All tests passed!", "tests": results} diff --git a/src/core/executor.py b/src/core/executor.py index 9055366..6e52b9a 100644 --- a/src/core/executor.py +++ b/src/core/executor.py @@ -5,6 +5,7 @@ import resource from .config import COMPILERS, validate_code from .security.sanitizer import sanitize_code +from .cheat import is_cheat_mode, make_all_passed_result MAX_MEMORY_MB = int(os.getenv("MAX_MEMORY_MB", 128)) MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", 1)) @@ -59,6 +60,10 @@ def execute_custom_code(code: str, lang: str) -> dict: def execute_code(code: str, lang: str, tests: list, timeout: int = None, templates: dict = None, rules: dict = None) -> dict: """Execute code against test cases with validation and templating.""" + # --- Cheat mode: skip all execution and return all-passed --- + if is_cheat_mode(): + return make_all_passed_result(tests) + if timeout is None: timeout = MAX_RUN_TIME if lang not in COMPILERS: