diff --git a/bridge/bridge_api.py b/bridge/bridge_api.py index 16358495f..1e032e523 100644 --- a/bridge/bridge_api.py +++ b/bridge/bridge_api.py @@ -659,4 +659,4 @@ def register_bridge_routes(app: Flask): app = Flask(__name__) register_bridge_routes(app) print("Bridge dev server on http://0.0.0.0:8096") - app.run(host="0.0.0.0", port=8096, debug=True) + app.run(host="0.0.0.0", port=8096, debug=False) diff --git a/contributor_registry.py b/contributor_registry.py index 5a9a1d952..81975620e 100644 --- a/contributor_registry.py +++ b/contributor_registry.py @@ -186,4 +186,4 @@ def approve_contributor(username): if __name__ == '__main__': if not os.path.exists(DB_PATH): init_db() - app.run(debug=True, host='0.0.0.0', port=5000) + app.run(debug=False, host='0.0.0.0', port=5000) diff --git a/explorer/app.py b/explorer/app.py index 872a1019c..9d7201ab4 100644 --- a/explorer/app.py +++ b/explorer/app.py @@ -134,4 +134,4 @@ def internal_error(error): return render_template('500.html'), 500 if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file + app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/faucet_service/IMPLEMENTATION_SUMMARY.md b/faucet_service/IMPLEMENTATION_SUMMARY.md index a30ac1071..c388ceae1 100644 --- a/faucet_service/IMPLEMENTATION_SUMMARY.md +++ b/faucet_service/IMPLEMENTATION_SUMMARY.md @@ -284,7 +284,7 @@ cd faucet_service python3 test_faucet_service.py # Start the service -python3 faucet_service.py --debug +python3 faucet_service.py # Test API curl http://localhost:8090/health diff --git a/faucet_service/README.md b/faucet_service/README.md index 2b1efbfa9..5b5be0428 100644 --- a/faucet_service/README.md +++ b/faucet_service/README.md @@ -37,7 +37,7 @@ python faucet_service.py python faucet_service.py --config faucet_config.local.yaml # Run with command-line overrides -python faucet_service.py --host 0.0.0.0 --port 9000 --debug +python faucet_service.py --host 0.0.0.0 --port 9000 ``` The faucet will start at `http://localhost:8090/faucet` @@ -85,7 +85,7 @@ distribution: |--------|---------|-------------| | `host` | `0.0.0.0` | Server bind address | | `port` | `8090` | Server port | -| `debug` | `false` | Enable debug mode | +| `debug` | `false` | Flask debug mode stays disabled | | `base_path` | `/faucet` | Base URL path | #### Rate Limiting @@ -430,13 +430,9 @@ redis-cli ping sqlite3 faucet.db "SELECT * FROM drip_requests LIMIT 5;" ``` -### Debug Mode +### Debugging -Enable debug mode for detailed logging: - -```bash -python faucet_service.py --debug -``` +Use application logs and health endpoints for diagnostics. Flask debug mode stays disabled for service entrypoints. ## License diff --git a/faucet_service/faucet_service.py b/faucet_service/faucet_service.py index 0a2cd2269..576673012 100644 --- a/faucet_service/faucet_service.py +++ b/faucet_service/faucet_service.py @@ -1146,7 +1146,8 @@ def main(): help='Path to configuration file') parser.add_argument('--host', help='Override host from config') parser.add_argument('--port', '-p', type=int, help='Override port from config') - parser.add_argument('--debug', action='store_true', help='Enable debug mode') + parser.add_argument('--debug', action='store_true', + help='Deprecated; Flask debug mode remains disabled') args = parser.parse_args() @@ -1158,8 +1159,7 @@ def main(): config['server']['host'] = args.host if args.port: config['server']['port'] = args.port - if args.debug: - config['server']['debug'] = True + config['server']['debug'] = False # Create and run app app = create_app(config) diff --git a/keeper_explorer.py b/keeper_explorer.py index 9f377e03a..1cd6f286f 100644 --- a/keeper_explorer.py +++ b/keeper_explorer.py @@ -401,4 +401,4 @@ def faucet_drip(): if __name__ == '__main__': import hashlib # needed for mock hash print(f"[*] Starting Fossil-Punk Keeper Explorer on port {PORT}...") - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=False) diff --git a/profile_badge_generator.py b/profile_badge_generator.py index 59e08b70d..143b58c22 100644 --- a/profile_badge_generator.py +++ b/profile_badge_generator.py @@ -246,4 +246,4 @@ def list_badges(): if __name__ == '__main__': init_badge_db() - app.run(debug=True, port=5003) + app.run(debug=False, port=5003) diff --git a/security_test_payment_widget.py b/security_test_payment_widget.py index 5bbc35b31..c9edaff70 100644 --- a/security_test_payment_widget.py +++ b/security_test_payment_widget.py @@ -272,4 +272,4 @@ def admin_login(): if __name__ == '__main__': if not os.path.exists(DB_PATH): init_db() - app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file + app.run(debug=False, host='0.0.0.0', port=5000) diff --git a/tests/test_flask_debug_disabled.py b/tests/test_flask_debug_disabled.py new file mode 100644 index 000000000..1edaef48e --- /dev/null +++ b/tests/test_flask_debug_disabled.py @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: MIT +import ast +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +PUBLIC_FLASK_ENTRYPOINTS = [ + ROOT / "bcos_directory.py", + ROOT / "bridge" / "bridge_api.py", + ROOT / "contributor_registry.py", + ROOT / "explorer" / "app.py", + ROOT / "faucet_service" / "faucet_service.py", + ROOT / "keeper_explorer.py", + ROOT / "profile_badge_generator.py", + ROOT / "security_test_payment_widget.py", +] + +LOCAL_POC_DEBUG_HARNESSES = { + "xss_poc_templates.py", +} + +PARSE_INCOMPATIBLE_PYTHON = { + # Existing static site builder uses syntax that is not parseable by this + # merge-gate probe. It is not a Flask entrypoint, so keep it documented here + # instead of letting the broad scan fail before checking the public surface. + "build_static.py", + # Existing node withdrawal validation test imports a dotted backup filename + # that cannot be parsed as Python syntax, outside the Flask debug surface. + "node/tests/test_withdrawal_validation.py", +} + + +def repo_path(path): + return str(path.relative_to(ROOT)).replace("\\", "/") + + +def is_debug_subscript(node): + return ( + isinstance(node, ast.Subscript) + and isinstance(node.slice, ast.Constant) + and node.slice.value == "debug" + ) + + +def debug_true_locations(path): + try: + tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) + except SyntaxError as exc: + relative = repo_path(path) + if relative in PARSE_INCOMPATIBLE_PYTHON: + return [] + raise AssertionError(f"{relative} could not be parsed by the Flask debug scan: {exc}") from exc + locations = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Call): + for keyword in node.keywords: + if ( + keyword.arg == "debug" + and isinstance(keyword.value, ast.Constant) + and keyword.value.value is True + ): + locations.append((node.lineno, "app.run debug=True")) + + if isinstance(node, ast.Assign): + assigns_true = isinstance(node.value, ast.Constant) and node.value.value is True + if assigns_true and any(is_debug_subscript(target) for target in node.targets): + locations.append((node.lineno, "debug config assignment to True")) + + return locations + + +def python_sources(): + skipped_dirs = {".git", ".mypy_cache", ".pytest_cache", "__pycache__", "venv", ".venv"} + for path in ROOT.rglob("*.py"): + if skipped_dirs.intersection(path.relative_to(ROOT).parts): + continue + yield path + + +def test_public_flask_entrypoints_do_not_enable_debug_mode(): + failures = { + str(path.relative_to(ROOT)): debug_true_locations(path) + for path in PUBLIC_FLASK_ENTRYPOINTS + } + failures = {path: locations for path, locations in failures.items() if locations} + + assert failures == {} + + +def test_no_undocumented_flask_debug_true_entrypoints(): + failures = {} + for path in python_sources(): + relative = repo_path(path) + if relative in LOCAL_POC_DEBUG_HARNESSES: + continue + locations = debug_true_locations(path) + if locations: + failures[relative] = locations + + assert failures == {}