393 files
This commit is contained in:
@@ -13,11 +13,15 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from ..const import (
|
||||
CONF_TASKS,
|
||||
TRIGGER_FIELD_RANGES,
|
||||
TRIGGER_RUNTIME_HOURS_MAX,
|
||||
UNAVAILABLE_STATES,
|
||||
TriggerType,
|
||||
)
|
||||
from ..helpers.aggregate import object_name
|
||||
from ..helpers.trigger_fallback import threshold_limits_overlap
|
||||
from ..helpers.url_safety import _SAFE_URL_SCHEMES as _SAFE_URL_SCHEMES
|
||||
from ..helpers.url_safety import is_safe_url
|
||||
from . import (
|
||||
_get_object_entries,
|
||||
)
|
||||
@@ -26,36 +30,8 @@ from . import (
|
||||
# Validation helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SAFE_URL_SCHEMES = {"http", "https"}
|
||||
|
||||
|
||||
def _is_safe_url(url: str | None) -> bool:
|
||||
"""Reject javascript:, data:, protocol-relative and other dangerous URLs.
|
||||
|
||||
Only http/https and genuine path-relative URLs (no host) pass. ASCII control
|
||||
characters and surrounding whitespace are stripped first, since urlparse and
|
||||
browsers ignore them and they can otherwise mask a "//host" or scheme-less
|
||||
host (e.g. ``" //evil.com"`` or ``"\t//evil.com"``).
|
||||
"""
|
||||
if not url:
|
||||
return True
|
||||
from urllib.parse import urlparse
|
||||
|
||||
cleaned = "".join(ch for ch in url if ch.isprintable()).strip()
|
||||
if not cleaned:
|
||||
return True
|
||||
# Block protocol-relative URLs like //evil.com
|
||||
if cleaned.startswith("//"):
|
||||
return False
|
||||
try:
|
||||
parsed = urlparse(cleaned)
|
||||
except Exception: # noqa: BLE001 - any malformed URL is rejected as unsafe
|
||||
return False
|
||||
scheme = parsed.scheme.lower()
|
||||
if scheme in _SAFE_URL_SCHEMES:
|
||||
return True
|
||||
# An empty scheme is only safe for a true path-relative URL with no host.
|
||||
return scheme == "" and not parsed.netloc
|
||||
# The URL rule lives in helpers.url_safety (every write path shares it).
|
||||
_is_safe_url = is_safe_url
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -285,12 +261,10 @@ _NUMBER_FIELDS: tuple[str, ...] = (
|
||||
"trigger_target_value",
|
||||
"trigger_baseline_value",
|
||||
)
|
||||
# Whole-number fields with an inclusive range.
|
||||
_INT_FIELDS: dict[str, tuple[int, int]] = {
|
||||
"trigger_for_minutes": (0, 1440),
|
||||
"trigger_target_changes": (1, 10_000),
|
||||
}
|
||||
TRIGGER_RUNTIME_HOURS_MAX = 100_000
|
||||
# Whole-number fields with an inclusive range — the bounds the options-flow
|
||||
# selectors use too (const.TRIGGER_FIELD_RANGES, DRY audit 2026-09-26 B).
|
||||
# TRIGGER_RUNTIME_HOURS_MAX is imported from const and re-exported here.
|
||||
_INT_FIELDS: dict[str, tuple[int, int]] = TRIGGER_FIELD_RANGES
|
||||
# Optional fields where an explicit null means "unset" — dropped rather than
|
||||
# refused, so a client clearing a field never trips the validator.
|
||||
_OPTIONAL_VALUE_FIELDS: tuple[str, ...] = (
|
||||
|
||||
Reference in New Issue
Block a user