Browse Source

Add SSRF mitigation for external camera URLs

Block access to cloud metadata services and dangerous destinations:
- AWS/GCP/Azure metadata endpoint (169.254.169.254)
- GCP internal metadata hostnames
- localhost and loopback addresses
- All link-local addresses (169.254.x.x)

Local network IPs (192.168.x.x, 10.x.x.x) are still allowed since
cameras are typically on the same LAN as the server.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
maziggy 3 months ago
parent
commit
2960261aa9
1 changed files with 31 additions and 4 deletions
  1. 31 4
      backend/app/services/external_camera.py

+ 31 - 4
backend/app/services/external_camera.py

@@ -21,11 +21,14 @@ logger = logging.getLogger(__name__)
 
 
 
 
 def _validate_camera_url(url: str, allowed_schemes: tuple[str, ...] = ("http", "https", "rtsp")) -> bool:
 def _validate_camera_url(url: str, allowed_schemes: tuple[str, ...] = ("http", "https", "rtsp")) -> bool:
-    """Validate camera URL format.
+    """Validate camera URL format and block dangerous destinations.
+
+    This validates that the URL is well-formed, uses an allowed scheme,
+    and does not target cloud metadata services.
 
 
-    This validates that the URL is well-formed and uses an allowed scheme.
     Note: This intentionally allows user-provided URLs as that is the
     Note: This intentionally allows user-provided URLs as that is the
-    purpose of external camera configuration.
+    purpose of external camera configuration. Local network IPs are
+    allowed since cameras are typically on the same LAN.
 
 
     Args:
     Args:
         url: URL to validate
         url: URL to validate
@@ -38,7 +41,31 @@ def _validate_camera_url(url: str, allowed_schemes: tuple[str, ...] = ("http", "
         parsed = urlparse(url)
         parsed = urlparse(url)
         if not parsed.scheme or not parsed.netloc:
         if not parsed.scheme or not parsed.netloc:
             return False
             return False
-        return parsed.scheme.lower() in allowed_schemes
+        if parsed.scheme.lower() not in allowed_schemes:
+            return False
+
+        # Block cloud metadata service endpoints (SSRF mitigation)
+        # These are dangerous destinations that should never be accessed
+        hostname = parsed.hostname or ""
+        blocked_hosts = (
+            "169.254.169.254",  # AWS/GCP/Azure metadata
+            "metadata.google.internal",  # GCP metadata
+            "metadata.google",
+            "localhost",  # Block localhost to prevent internal service access
+            "127.0.0.1",
+            "::1",
+            "0.0.0.0",
+        )
+        if hostname.lower() in blocked_hosts:
+            logger.warning(f"Blocked camera URL targeting restricted host: {hostname}")
+            return False
+
+        # Block link-local addresses (169.254.x.x)
+        if hostname.startswith("169.254."):
+            logger.warning(f"Blocked camera URL targeting link-local address: {hostname}")
+            return False
+
+        return True
     except Exception:
     except Exception:
         return False
         return False