Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 2024-05-23 - [Input Validation and Syntax Fix]
**Vulnerability:** The `create_folder` function contained a syntax error (positional arg after keyword arg) preventing execution. Additionally, `folder_url` and `profile_id` lacked validation, potentially allowing SSRF (via non-HTTPS URLs) or path traversal/injection (via crafted profile IDs).
**Learning:** Even simple scripts need robust input validation, especially when inputs are used to construct URLs or file paths. A syntax error can mask security issues by preventing the code from running in the first place.
**Prevention:**
1. Always validate external inputs against a strict allowlist (e.g., regex for IDs, protocol check for URLs).
2. Use linters/static analysis to catch syntax errors before runtime.
25 changes: 24 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import os
import logging
import time
import re
from typing import Dict, List, Optional, Any, Set, Sequence

import httpx
Expand Down Expand Up @@ -97,6 +98,22 @@ def _api_client() -> httpx.Client:
_cache: Dict[str, Dict] = {}


def validate_folder_url(url: str) -> bool:
"""Validate that the folder URL is safe (HTTPS only)."""
if not url.startswith("https://"):
log.warning(f"Skipping unsafe or invalid URL: {url}")
return False
return True
Comment on lines +101 to +106
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The folder URL validation only checks for the HTTPS protocol but doesn't validate the host. This could still allow SSRF attacks against internal services accessible via HTTPS (e.g., "https://localhost:8080/admin" or "https://192.168.1.1/"). Consider adding an allowlist of trusted domains (e.g., only allowing github.com and githubusercontent.com) or at minimum blocking private IP ranges and localhost.

Copilot uses AI. Check for mistakes.


def validate_profile_id(profile_id: str) -> bool:
"""Validate that the profile ID contains only safe characters."""
if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id):
log.error(f"Invalid profile ID format: {profile_id}")
return False
return True


def _api_get(client: httpx.Client, url: str) -> httpx.Response:
"""GET helper for Control-D API with retries."""
return _retry_request(lambda: client.get(url))
Expand Down Expand Up @@ -227,9 +244,9 @@ def create_folder(client: httpx.Client, profile_id: str, name: str, do: int, sta
"""
try:
_api_post(
client,
f"{API_BASE}/{profile_id}/groups",
data={"name": name, "do": do, "status": status},
client,
)

# Re-fetch the list and pick the folder we just created
Expand Down Expand Up @@ -334,6 +351,8 @@ def sync_profile(
# Fetch all folder data first
folder_data_list = []
for url in folder_urls:
if not validate_folder_url(url):
continue
try:
folder_data_list.append(fetch_folder_data(url))
except (httpx.HTTPError, KeyError) as e:
Expand Down Expand Up @@ -459,6 +478,10 @@ def main():
plan: List[Dict[str, Any]] = []
success_count = 0
for profile_id in (profile_ids or ["dry-run-placeholder"]):
# Skip validation for dry-run placeholder
if profile_id != "dry-run-placeholder" and not validate_profile_id(profile_id):
continue

log.info("Starting sync for profile %s", profile_id)
if sync_profile(profile_id, folder_urls, dry_run=args.dry_run, no_delete=args.no_delete, plan_accumulator=plan):
success_count += 1
Expand Down
Loading