HearthNet / docs /SECURITY_FINDINGS.md
GitHub Actions
sec: remove API key from Gradio frontend value β€” SEC-1 critical fix
57f29b0
|
Raw
History Blame Contribute Delete
6.99 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

HearthNet β€” Security Findings & Fixes

Audit date: June 15, 2026


CRITICAL β€” Fixed βœ…

SEC-1: NVIDIA API Key Exposed in Frontend HTML

Severity: Critical
Status: Fixed in commit (June 15, 2026)

What happened:
app_nemotron.py and hearthnet/ui/tabs/nemotron.py both passed the live API key as the value= parameter of a gr.Textbox component:

# VULNERABLE (before fix)
api_key_box = gr.Textbox(
    label="πŸ”‘ NVIDIA API Key",
    value=_NVIDIA_KEY,          # ← actual secret sent to browser
    type="password",
    ...
)

Gradio serializes all component initial values into the page's JavaScript state (window.__gradio_state__ / WebSocket init message). Even though the field is rendered as type="password" (dots in the UI), the underlying value is present in:

  • The page source (view-source:)
  • The Network tab β†’ WS frames or the initial /info response
  • document.querySelector('[data-testid=...]').value in the Console tab

How to reproduce (before fix):

  1. Open the Nemotron Space in Chrome
  2. Open DevTools β†’ Network tab
  3. Reload page; inspect the first Gradio WebSocket frame or /__/info response
  4. Search for nvapi- β€” the full key appears in plaintext

Fix applied:

# SAFE (after fix)
api_key_box = gr.Textbox(
    label="πŸ”‘ NVIDIA API Key",
    value="",                   # ← always empty; key stays server-side
    type="password",
    placeholder="nvapi-... leave blank if NVIDIA_API_KEY env var is set",
)

The server-side handlers already fall back to the env var:

key = api_key.strip() or _NVIDIA_KEY   # env var used if textbox is empty

So if NVIDIA_API_KEY is set as a Space secret, users never need to type it.

Files changed:

  • app_nemotron.py:288 β€” value=_NVIDIA_KEY β†’ value=""
  • hearthnet/ui/tabs/nemotron.py:100 β€” value=api_key_env β†’ value=""

MEDIUM β€” Action Required

SEC-2: API Key Typed by User Travels as Plaintext in POST Body

Severity: Medium
Status: Mitigated by HTTPS, not yet end-to-end encrypted

If a user manually types an API key into the textbox (e.g. when running locally over HTTP), the key is sent in the Gradio WebSocket message body when the button is clicked. On HF Spaces this is HTTPS so the transport is encrypted. On local HTTP it is not.

Recommendation:
For local deployments, document that the API key textbox is for development only. For production, always use NVIDIA_API_KEY env var (Space secret) and leave the textbox empty. Add a warning label when the Space is detected as HTTP:

is_https = os.getenv("SPACE_HOST", "").startswith("https")
if not is_https:
    gr.Markdown("⚠ Running over HTTP β€” use env var, not the key textbox")

SEC-3: Rate Limiting Not Enforced on Capability Bus Endpoints

Severity: Medium
Status: Open β€” RateLimiter class implemented but not wired

hearthnet/bus/backpressure.py contains a working RateLimiter(max_calls, window_seconds). The FastAPI routes at /bus/v1/call, /relay/v1/join, /relay/v1/send are publicly accessible with no rate limiting. A malicious client can:

  • Exhaust ZeroGPU quota by spamming llm.chat calls
  • Flood the relay hub roster with fake node registrations

How to fix:

# In app.py, _mount_bus_endpoints():
from hearthnet.bus.backpressure import RateLimiter

_limiter = RateLimiter(max_calls=60, window_seconds=60)

@app.middleware("http")
async def _rate_limit_middleware(request, call_next):
    client_ip = request.client.host if request.client else "unknown"
    if request.url.path.startswith(("/bus/v1", "/relay/v1")):
        if not _limiter.allow(client_ip):
            from fastapi.responses import JSONResponse
            return JSONResponse({"error": "rate_limited"}, status_code=429)
    return await call_next(request)

SEC-4: Capability Token Expiry Not Enforced

Severity: Medium
Status: Open β€” exp field stored but never checked

M16 capability tokens (hearthnet/tokens/) store an exp (expiry) timestamp in the JWT-like structure, but the router (hearthnet/bus/router.py) never validates it before routing a call. An expired token continues to work indefinitely.

How to fix:

# In hearthnet/bus/router.py, before routing:
import time
token_exp = getattr(token, "exp", None)
if token_exp and time.time() > token_exp:
    raise PermissionError("capability token expired")

SEC-5: trust_remote_code=True in Florence2 Backend

Severity: Medium
Status: Partially mitigated β€” allowlist added (June 12 security audit)

hearthnet/services/image/backends/florence2.py loads the Florence2 model with trust_remote_code=True. The allowlist restricts which model IDs are permitted, but if a mesh peer can influence MODEL_ID via a capability call, arbitrary code execution is possible.

Recommendation:
Pin the Florence2 model to a known-good hash in pyproject.toml or hardcode the model ID rather than reading it from a bus payload.


LOW β€” Informational

SEC-6: Mesh Node URL Visible in Frontend

Severity: Low
Status: Acceptable β€” not a secret

app_nemotron.py:421 passes value=_MESH_NODE to a visible (non-password) textbox. HEARTHNET_NODE is a public Space URL, not a credential. This is intentional so users can see which node they are pushing to.

No action required unless the node URL is considered sensitive.

SEC-7: Relay Roster is Publicly Readable

Severity: Low
Status: By design β€” open mesh

GET /relay/v1/roster returns the full list of connected nodes with their node_id, display_name, community_id, and capabilities. There is no authentication on this endpoint.

Acceptable for a public hackathon mesh. For a production deployment, consider requiring a community token to read the roster.

SEC-8: SQLite Event Log Has No Encryption at Rest

Severity: Low
Status: Open

The SQLite databases at ~/.hearthnet/corpora/*.db and the relay roster DB store conversation history, RAG documents, and node rosters in plaintext.

Recommendation for production: Use SQLCipher or encrypt sensitive fields before writing to the DB.


General Security Principles to Follow

  1. Never pass secrets as Gradio value= β€” they go into page state. Always use value="" and read env vars server-side.

  2. type="password" only hides visually β€” it does not encrypt the value in the WebSocket or page source.

  3. HF Space secrets are the correct mechanism. Set NVIDIA_API_KEY, MODAL_ENDPOINT, HEARTHNET_NODE as Space secrets β€” they are injected as env vars at runtime and never appear in the repo or page HTML.

  4. HTTPS is mandatory for any textbox that accepts credentials. Local dev over HTTP should use env vars only, never the textbox.

  5. Rate-limit all public endpoints before opening the Space to external traffic.