Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/secuscan/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Settings(BaseSettings):
# Server Configuration
bind_address: str = "127.0.0.1"
bind_port: int = 8000
debug: bool = True
debug: bool = False

# Primary data store
database_path: str = str(PROJECT_ROOT / "data" / "secuscan.db")
Expand Down
23 changes: 17 additions & 6 deletions backend/secuscan/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
SecuScan Backend - Main application entry point
"""

import html as html_mod
import logging
import sys
import shutil
Expand Down Expand Up @@ -162,30 +163,39 @@ async def lifespan(app: FastAPI):
await scheduler.stop()
logger.info("✓ Shutdown complete")

# Create FastAPI application
# Create FastAPI application — docs/redoc/openapi only exposed when debug=True
app = FastAPI(
title="SecuScan API",
description="Backend for SecuScan Pentesting Toolkit",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
docs_url="/docs" if settings.debug else None,
redoc_url="/redoc" if settings.debug else None,
openapi_url="/openapi.json" if settings.debug else None,
Comment thread
namann5 marked this conversation as resolved.
lifespan=lifespan
)

@app.get("/api/docs", include_in_schema=False)
async def redirect_api_docs():
from fastapi.responses import RedirectResponse
if not settings.debug:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Not found")
return RedirectResponse(url="/docs")

@app.get("/api/redoc", include_in_schema=False)
async def redirect_api_redoc():
from fastapi.responses import RedirectResponse
if not settings.debug:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Not found")
return RedirectResponse(url="/redoc")

@app.get("/api/openapi.json", include_in_schema=False)
async def redirect_api_openapi():
from fastapi.responses import RedirectResponse
if not settings.debug:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Not found")
return RedirectResponse(url="/openapi.json")

# CORS middleware
Expand Down Expand Up @@ -280,8 +290,9 @@ async def custom_unhandled_exception_handler(request: Request, exc: Exception):

if settings.debug:
import traceback
html = f"<html><body><h1>500 Internal Server Error</h1><pre>{traceback.format_exc()}</pre></body></html>"
response = HTMLResponse(html, status_code=500)
safe_traceback = html_mod.escape(traceback.format_exc())
html_content = f"<html><body><h1>500 Internal Server Error</h1><pre>{safe_traceback}</pre></body></html>"
response = HTMLResponse(html_content, status_code=500)
else:
response = PlainTextResponse("Internal Server Error", status_code=500)

Expand Down
3 changes: 3 additions & 0 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pip install -q -r backend/requirements.txt
mkdir -p "$ROOT_DIR/data" "$ROOT_DIR/logs"

echo "🚀 Starting backend on http://127.0.0.1:8000"
# Enable debug mode for development — exposes /docs, /redoc, /openapi.json
# and serves detailed error pages. Production defaults to debug=False.
export SECUSCAN_DEBUG=true
python -m uvicorn backend.secuscan.main:app \
--host 127.0.0.1 \
--port 8000 \
Expand Down
90 changes: 88 additions & 2 deletions testing/backend/unit/test_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ def test_settings_default_bind_address():


def test_settings_default_debug():
"""Debug defaults to True."""
"""Debug defaults to False (secure-by-default)."""
s = Settings()
assert s.debug is True
assert s.debug is False


def test_settings_default_sandbox_disabled():
Expand Down Expand Up @@ -286,3 +286,89 @@ def test_sandbox_settings_env_override():
assert s.sandbox_timeout == 30
assert s.sandbox_memory_mb == 128
assert s.sandbox_allow_network is False


# ── debug mode docs gating (Issue #2022) ──────────────────────────────────────


def test_debug_mode_true_enables_docs():
"""When debug=True, docs_url/redoc_url/openapi_url are set."""
s = Settings(debug=True)
assert s.debug is True


def test_debug_mode_false_disables_docs():
"""When debug=False (default), docs should not be exposed."""
s = Settings(debug=False)
assert s.debug is False


# ── FastAPI docs gating via TestClient ────────────────────────────────────────


def test_docs_disabled_when_debug_false(tmp_path, monkeypatch):
"""/docs returns 404 when the app was created with debug=False."""
from backend.secuscan.main import app as _app
from fastapi.testclient import TestClient

# The app is created with debug=False by default, so docs_url=None
assert _app.docs_url is None

with TestClient(_app, raise_server_exceptions=False) as client:
resp = client.get("/docs")
assert resp.status_code == 404


def test_openapi_disabled_when_debug_false(tmp_path, monkeypatch):
"""/openapi.json returns 404 when the app was created with debug=False."""
from backend.secuscan.main import app as _app
from fastapi.testclient import TestClient

# The app is created with debug=False by default, so openapi_url=None
assert _app.openapi_url is None

with TestClient(_app, raise_server_exceptions=False) as client:
resp = client.get("/openapi.json")
assert resp.status_code == 404


def test_docs_enabled_when_debug_true(tmp_path):
"""/docs returns 200 when a fresh app is created with debug=True."""
from backend.secuscan.config import Settings
from fastapi import FastAPI
from fastapi.testclient import TestClient

app_debug = FastAPI(
title="Test",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)

with TestClient(app_debug, raise_server_exceptions=False) as client:
resp = client.get("/docs")
assert resp.status_code == 200
resp = client.get("/openapi.json")
assert resp.status_code == 200
data = resp.json()
assert "openapi" in data


def test_traceback_not_leaked_in_production(tmp_path, monkeypatch):
"""In production (debug=False), unhandled exceptions return plain text, not HTML tracebacks."""
from backend.secuscan.config import settings as real_settings
from backend.secuscan.main import app as _app
from fastapi.testclient import TestClient

monkeypatch.setattr(real_settings, "debug", False)

@_app.get("/test-exception-prod")
async def raise_exception_prod():
raise RuntimeError("secret-sauce-error-12345")

with TestClient(_app, raise_server_exceptions=False) as client:
resp = client.get("/test-exception-prod")
assert resp.status_code == 500
body = resp.text
assert "secret-sauce-error-12345" not in body
assert "<pre>" not in body
Loading