Files
gh-basher83-lunar-claude-pl…/skills/python-uv-scripts/reference/security-patterns.md
2025-11-29 18:00:18 +08:00

11 KiB

Security Patterns for uv Scripts

Security best practices for Python single-file scripts following Virgo-Core patterns.

Never Hardcode Secrets

Anti-Pattern: Hardcoded Secrets

# NEVER DO THIS
API_KEY = "sk_live_1234567890abcdef"
DATABASE_URL = "postgresql://user:password@localhost/db"
PROXMOX_PASSWORD = "admin123"

Risks:

  • Secrets exposed in version control
  • No audit trail
  • Difficult to rotate
  • Same credentials across environments

Pattern 1: Environment Variables

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///

import os
import sys

def get_secret(name: str, required: bool = True) -> str:
    """Get secret from environment"""
    value = os.getenv(name)

    if required and not value:
        print(f"Error: {name} environment variable not set", file=sys.stderr)
        sys.exit(1)

    return value

# Usage
PROXMOX_PASSWORD = get_secret("PROXMOX_PASSWORD")
API_URL = get_secret("PROXMOX_API_URL")

Usage:

export PROXMOX_PASSWORD="$(cat ~/.secrets/proxmox_pass)"
./script.py

Pattern 2: Keyring Integration

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "keyring>=24.0.0",
# ]
# ///

import keyring
import sys

def get_password(service: str, username: str) -> str:
    """Get password from system keyring"""
    password = keyring.get_password(service, username)

    if not password:
        print(f"Error: No password found for {username}@{service}", file=sys.stderr)
        print(f"Set with: keyring set {service} {username}", file=sys.stderr)
        sys.exit(1)

    return password

# Usage
proxmox_password = get_password("proxmox", "terraform@pam")

Setup:

# Store password in system keyring
keyring set proxmox terraform@pam
# Prompts for password, stores securely

# Run script (no password in environment)
./script.py

Pattern 3: Infisical Integration (Repository Standard)

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "infisical-python>=2.3.3",
# ]
# ///

from infisical import InfisicalClient
import os
import sys

def get_infisical_secret(
    secret_name: str,
    project_id: str,
    environment: str = "prod",
    path: str = "/"
) -> str:
    """Get secret from Infisical vault"""
    try:
        client = InfisicalClient()
        secret = client.get_secret(
            secret_name=secret_name,
            project_id=project_id,
            environment=environment,
            path=path
        )
        return secret.secret_value
    except Exception as e:
        print(f"Error retrieving secret {secret_name}: {e}", file=sys.stderr)
        sys.exit(1)

# Usage (following Virgo-Core pattern)
PROXMOX_PASSWORD = get_infisical_secret(
    secret_name="PROXMOX_PASSWORD",
    project_id="7b832220-24c0-45bc-a5f1-ce9794a31259",
    environment="prod",
    path="/matrix"
)

Setup:

# Authenticate with Infisical
infisical login

# Run script (secrets fetched automatically)
./script.py

Input Validation

Anti-Pattern: No Validation

import subprocess

# DANGEROUS - Command injection risk
def ping_host(hostname: str):
    subprocess.run(f"ping -c 3 {hostname}", shell=True)

# User provides: "localhost; rm -rf /"
ping_host(sys.argv[1])  # 💥 System destroyed

Pattern: Validate All Inputs

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///

import re
import sys
import subprocess

def validate_hostname(hostname: str) -> bool:
    """Validate hostname format"""
    # Only allow alphanumeric, dots, hyphens
    pattern = r'^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$'
    if not re.match(pattern, hostname):
        return False

    # Max length check
    if len(hostname) > 253:
        return False

    return True

def ping_host(hostname: str):
    """Safely ping a host"""
    if not validate_hostname(hostname):
        print(f"Error: Invalid hostname: {hostname}", file=sys.stderr)
        sys.exit(1)

    # Use list form (no shell injection)
    result = subprocess.run(
        ["ping", "-c", "3", hostname],
        capture_output=True,
        text=True
    )

    return result.returncode == 0

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: ping_host.py <hostname>", file=sys.stderr)
        sys.exit(1)

    ping_host(sys.argv[1])

Input Validation Patterns

import re
from ipaddress import IPv4Address, AddressValueError

def validate_ip(ip: str) -> bool:
    """Validate IPv4 address"""
    try:
        IPv4Address(ip)
        return True
    except AddressValueError:
        return False

def validate_port(port: int) -> bool:
    """Validate TCP/UDP port number"""
    return 1 <= port <= 65535

def validate_vmid(vmid: int) -> bool:
    """Validate Proxmox VMID (100-999999999)"""
    return 100 <= vmid <= 999999999

def validate_path(path: str) -> bool:
    """Validate file path (no directory traversal)"""
    # Reject paths with ../
    if ".." in path:
        return False

    # Reject absolute paths
    if path.startswith("/"):
        return False

    return True

Dependency Security

Pin Dependencies

# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "httpx>=0.27.0",      # ✅ Minimum version pinned
#   "rich>=13.0.0",       # ✅ Known good version
# ]
# ///

Why pin?

  • Prevents automatic upgrades with vulnerabilities
  • Ensures reproducible execution
  • Allows controlled dependency updates

Exclude Recent Packages

# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx>=0.27.0"]
#
# [tool.uv]
# exclude-newer = "2024-10-01T00:00:00Z"  # Only packages before this date
# ///

Use cases:

  • Prevent supply chain attacks from compromised packages
  • Freeze dependencies at known-good state
  • Reproducible builds in CI/CD

Check for Vulnerabilities

# Use safety or pip-audit
uv pip install safety
safety check --json

# Or use built-in tools
uv pip list --format json | jq '.[] | select(.name == "httpx")'

File Operations Security

Anti-Pattern: Unsafe File Access

# DANGEROUS - Path traversal vulnerability
def read_log(filename: str):
    with open(f"/var/log/{filename}") as f:
        return f.read()

# User provides: "../../../etc/passwd"
read_log(sys.argv[1])  # 💥 Reads /etc/passwd

Pattern: Safe File Operations

import os
import sys
from pathlib import Path

def safe_read_log(filename: str, log_dir: str = "/var/log") -> str:
    """Safely read log file"""
    # Resolve to absolute paths
    log_dir_path = Path(log_dir).resolve()
    file_path = (log_dir_path / filename).resolve()

    # Ensure file is within log directory
    try:
        file_path.relative_to(log_dir_path)
    except ValueError:
        print(f"Error: Path traversal detected: {filename}", file=sys.stderr)
        sys.exit(1)

    # Check file exists and is readable
    if not file_path.exists():
        print(f"Error: File not found: {filename}", file=sys.stderr)
        sys.exit(1)

    if not file_path.is_file():
        print(f"Error: Not a file: {filename}", file=sys.stderr)
        sys.exit(1)

    # Read with size limit
    MAX_SIZE = 10 * 1024 * 1024  # 10MB
    if file_path.stat().st_size > MAX_SIZE:
        print(f"Error: File too large: {filename}", file=sys.stderr)
        sys.exit(1)

    with open(file_path) as f:
        return f.read()

Command Execution Security

Anti-Pattern: Shell Injection

# DANGEROUS
import subprocess

def list_directory(path: str):
    subprocess.run(f"ls -la {path}", shell=True)

# User provides: "; rm -rf /"
list_directory(sys.argv[1])  # 💥 Disaster

Pattern: Safe Command Execution

import subprocess
import sys

def list_directory(path: str):
    """Safely list directory contents"""
    # Validate path first
    if not validate_path(path):
        print(f"Error: Invalid path: {path}", file=sys.stderr)
        sys.exit(1)

    # Use list form (no shell)
    try:
        result = subprocess.run(
            ["ls", "-la", path],
            capture_output=True,
            text=True,
            check=True,
            timeout=5  # Prevent hanging
        )
        print(result.stdout)
    except subprocess.CalledProcessError as e:
        print(f"Error: {e.stderr}", file=sys.stderr)
        sys.exit(1)
    except subprocess.TimeoutExpired:
        print("Error: Command timed out", file=sys.stderr)
        sys.exit(1)

Logging and Audit

Secure Logging

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "structlog>=24.0.0",
# ]
# ///

import structlog
import sys

# Configure structured logging
structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer(),
    ],
    logger_factory=structlog.PrintLoggerFactory(file=sys.stderr),
)

log = structlog.get_logger()

def process_data(user_id: int, action: str):
    """Process user action with audit logging"""
    log.info(
        "user_action",
        user_id=user_id,
        action=action,
        # Don't log sensitive data!
    )

    # ... processing logic ...

    log.info(
        "action_completed",
        user_id=user_id,
        action=action,
        status="success"
    )

Never Log Secrets

# ❌ BAD - Logs password
log.info(f"Connecting with password: {password}")

# ✅ GOOD - No sensitive data
log.info("Connecting to API", endpoint=api_url)

# ✅ GOOD - Masked credentials
log.info("Authentication successful", user=username)

Network Security

HTTPS Only

#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["httpx>=0.27.0"]
# ///

import httpx
import sys

def fetch_api(url: str) -> dict:
    """Fetch data from API (HTTPS only)"""
    # Validate HTTPS
    if not url.startswith("https://"):
        print("Error: Only HTTPS URLs allowed", file=sys.stderr)
        sys.exit(1)

    with httpx.Client(verify=True) as client:  # Verify SSL certs
        response = client.get(url, timeout=10.0)
        response.raise_for_status()
        return response.json()

Certificate Verification

import httpx
import certifi

# Use trusted CA bundle
client = httpx.Client(verify=certifi.where())

# For internal CAs, specify cert path
client = httpx.Client(verify="/path/to/internal-ca.pem")

# ONLY disable for development (never in production)
if os.getenv("DEVELOPMENT") == "true":
    client = httpx.Client(verify=False)

Security Checklist

Before deploying a script:

  • No hardcoded secrets
  • Secrets from environment/keyring/Infisical
  • All inputs validated
  • No shell=True in subprocess
  • File paths checked for traversal
  • Dependencies pinned
  • No sensitive data in logs
  • HTTPS for network requests
  • Certificate verification enabled
  • Timeouts on external calls
  • Error messages don't leak information
  • Principle of least privilege applied

References