Compare commits

...

4 Commits

Author SHA1 Message Date
49097ba09e security: add atomic write utilities for TOCTOU protection (V-015)
Some checks failed
Docker Build and Publish / build-and-push (pull_request) Failing after 1m11s
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 33s
Tests / test (pull_request) Failing after 31s
Add atomic_write.py with temp file + rename pattern to prevent
Time-of-Check to Time-of-Use race conditions in file operations.

CVSS: 7.4 (High)
Refs: V-015
CWE-367: TOCTOU Race Condition
2026-03-31 00:08:54 +00:00
f3bfc7c8ad Merge pull request '[SECURITY] Prevent Error Information Disclosure (V-013, CVSS 7.5)' (#67) from security/fix-error-disclosure into main
Some checks failed
Nix / nix (ubuntu-latest) (push) Failing after 4s
Tests / test (push) Failing after 15s
Docker Build and Publish / build-and-push (push) Failing after 42s
Nix / nix (macos-latest) (push) Has been cancelled
2026-03-31 00:07:03 +00:00
5d0cf71a8b security: prevent error information disclosure (V-013, CVSS 7.5)
Some checks failed
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 30s
Tests / test (pull_request) Failing after 27s
Docker Build and Publish / build-and-push (pull_request) Failing after 38s
Add secure error handling to prevent internal details leaking.

Changes:
- gateway/platforms/api_server.py:
  - Add _handle_error_securely() function
  - Logs full error details with reference ID internally
  - Returns generic error message to client
  - Updates all cron job exception handlers to use secure handler

CVSS: 7.5 (High)
Refs: V-013 in SECURITY_AUDIT_REPORT.md
CWE-209: Generation of Error Message Containing Sensitive Information
2026-03-31 00:06:58 +00:00
3e0d3598bf Merge pull request '[SECURITY] Add Rate Limiting to API Server (V-016, CVSS 7.3)' (#66) from security/add-rate-limiting into main
Some checks failed
Nix / nix (ubuntu-latest) (push) Failing after 16s
Tests / test (push) Failing after 26s
Docker Build and Publish / build-and-push (push) Failing after 56s
Nix / nix (macos-latest) (push) Has been cancelled
2026-03-31 00:05:01 +00:00
2 changed files with 111 additions and 8 deletions

View File

@@ -207,6 +207,37 @@ def _openai_error(message: str, err_type: str = "invalid_request_error", param:
}
# SECURITY FIX (V-013): Safe error handling to prevent info disclosure
def _handle_error_securely(exception: Exception, context: str = "") -> Dict[str, Any]:
"""Handle errors securely - log full details, return generic message.
Prevents information disclosure by not exposing internal error details
to API clients. Logs full stack trace internally for debugging.
Args:
exception: The caught exception
context: Additional context about where the error occurred
Returns:
OpenAI-style error response with generic message
"""
import traceback
# Log full error details internally
error_id = str(uuid.uuid4())[:8]
logger.error(
f"Internal error [{error_id}] in {context}: {exception}\n"
f"{traceback.format_exc()}"
)
# Return generic error to client - no internal details
return _openai_error(
message=f"An internal error occurred. Reference: {error_id}",
err_type="internal_error",
code="internal_error"
)
if AIOHTTP_AVAILABLE:
@web.middleware
async def body_limit_middleware(request, handler):
@@ -1084,7 +1115,8 @@ class APIServerAdapter(BasePlatformAdapter):
jobs = self._cron_list(include_disabled=include_disabled)
return web.json_response({"jobs": jobs})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_create_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs — create a new cron job."""
@@ -1132,7 +1164,8 @@ class APIServerAdapter(BasePlatformAdapter):
job = self._cron_create(**kwargs)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_get_job(self, request: "web.Request") -> "web.Response":
"""GET /api/jobs/{job_id} — get a single cron job."""
@@ -1151,7 +1184,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_update_job(self, request: "web.Request") -> "web.Response":
"""PATCH /api/jobs/{job_id} — update a cron job."""
@@ -1184,7 +1218,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_delete_job(self, request: "web.Request") -> "web.Response":
"""DELETE /api/jobs/{job_id} — delete a cron job."""
@@ -1203,7 +1238,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"ok": True})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_pause_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/pause — pause a cron job."""
@@ -1222,7 +1258,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_resume_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/resume — resume a paused cron job."""
@@ -1241,7 +1278,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
async def _handle_run_job(self, request: "web.Request") -> "web.Response":
"""POST /api/jobs/{job_id}/run — trigger immediate execution."""
@@ -1260,7 +1298,8 @@ class APIServerAdapter(BasePlatformAdapter):
return web.json_response({"error": "Job not found"}, status=404)
return web.json_response({"job": job})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
# SECURITY FIX (V-013): Use secure error handling
return web.json_response(_handle_error_securely(e, "list_jobs"), status=500)
# ------------------------------------------------------------------
# Output extraction helper

64
tools/atomic_write.py Normal file
View File

@@ -0,0 +1,64 @@
"""Atomic file write operations to prevent TOCTOU race conditions.
SECURITY FIX (V-015): Implements atomic writes using temp files + rename
to prevent Time-of-Check to Time-of-Use race conditions.
CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
"""
import os
import tempfile
from pathlib import Path
from typing import Union
def atomic_write(path: Union[str, Path], content: str, mode: str = "w") -> None:
"""Atomically write content to file using temp file + rename.
This prevents TOCTOU race conditions where the file could be
modified between checking permissions and writing.
Args:
path: Target file path
content: Content to write
mode: Write mode ("w" for text, "wb" for bytes)
"""
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
# Write to temp file in same directory (same filesystem for atomic rename)
fd, temp_path = tempfile.mkstemp(
dir=path.parent,
prefix=f".tmp_{path.name}.",
suffix=".tmp"
)
try:
if "b" in mode:
os.write(fd, content if isinstance(content, bytes) else content.encode())
else:
os.write(fd, content.encode() if isinstance(content, str) else content)
os.fsync(fd) # Ensure data is written to disk
finally:
os.close(fd)
# Atomic rename - this is guaranteed to be atomic on POSIX
os.replace(temp_path, path)
def safe_read_write(path: Union[str, Path], content: str) -> dict:
"""Safely read and write file with TOCTOU protection.
Returns:
dict with status and error message if any
"""
try:
# SECURITY: Use atomic write to prevent race conditions
atomic_write(path, content)
return {"success": True, "error": None}
except PermissionError as e:
return {"success": False, "error": f"Permission denied: {e}"}
except OSError as e:
return {"success": False, "error": f"OS error: {e}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {e}"}