mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-05 17:31:06 +01:00
CodeQL fixes (#763)
- Block SSRF in image cover proxy (validate URL scheme and reject private IPs) - Sanitize settings tab name to prevent path traversal
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
"""Disk-based image cache with LRU eviction."""
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
@@ -466,6 +469,32 @@ class ImageCacheService:
|
||||
'hit_rate': round(hit_rate, 1),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_safe_url(url: str) -> bool:
|
||||
"""Check that a URL is safe to fetch (no SSRF to internal resources)."""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
return False
|
||||
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
return False
|
||||
|
||||
try:
|
||||
resolved = socket.getaddrinfo(hostname, None)
|
||||
for _, _, _, _, sockaddr in resolved:
|
||||
ip = ipaddress.ip_address(sockaddr[0])
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
|
||||
return False
|
||||
except (socket.gaierror, ValueError):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fetch_and_cache(self, cache_id: str, url: str) -> Optional[Tuple[bytes, str]]:
|
||||
"""Fetch an image from URL and cache it.
|
||||
|
||||
@@ -477,6 +506,9 @@ class ImageCacheService:
|
||||
Tuple of (image_data, content_type) or None on failure
|
||||
"""
|
||||
try:
|
||||
if not self._is_safe_url(url):
|
||||
logger.warning(f"Blocked request to disallowed URL: {url}")
|
||||
return None
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
|
||||
@@ -348,7 +348,11 @@ def _get_config_file_path(tab_name: str) -> Path:
|
||||
# Core settings tabs share the main settings.json file
|
||||
if tab_name in ("general", "search_mode"):
|
||||
return config_dir / "settings.json"
|
||||
return config_dir / "plugins" / f"{tab_name}.json"
|
||||
# Sanitize tab_name to prevent path traversal
|
||||
safe_name = Path(tab_name).name
|
||||
if not safe_name or safe_name != tab_name:
|
||||
raise ValueError(f"Invalid tab name: {tab_name}")
|
||||
return config_dir / "plugins" / f"{safe_name}.json"
|
||||
|
||||
|
||||
def _ensure_config_dir(tab_name: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user