diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 77db4f93..bc9cea4c 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -136,9 +136,22 @@ jobs: run: | python3 <<'PY' import os + import re import sys from urllib.parse import urlparse + DISALLOWED_PLACEHOLDERS = { + "", + "-", + "_", + "false", + "true", + "null", + "undefined", + "none", + "disabled", + } + def normalize(name: str) -> str: value = os.getenv(name, "").strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): @@ -150,9 +163,28 @@ jobs: return value return f"https://{value}" + def normalize_hostname(hostname: str) -> str: + normalized = hostname.strip().rstrip('.').lower() + if normalized.startswith('[') and normalized.endswith(']'): + normalized = normalized[1:-1] + return normalized + + def is_ip_address(hostname: str) -> bool: + if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname): + return all(0 <= int(part) <= 255 for part in hostname.split('.')) + return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None + + def is_allowed_hostname(hostname: str) -> bool: + normalized = normalize_hostname(hostname) + if not normalized or normalized in DISALLOWED_PLACEHOLDERS: + return False + if normalized == 'localhost': + return True + return '.' in normalized or is_ip_address(normalized) + def is_http_url(value: str) -> bool: parsed = urlparse(normalize_http_like(value)) - return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "") values = { name: normalize(name) @@ -167,13 +199,13 @@ jobs: for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"): value = values[name] - if not value: - errors.append(f"{name} must be set for release builds") + if value.lower() in DISALLOWED_PLACEHOLDERS: + errors.append(f"{name} must be set to a real value, not a placeholder") elif not is_http_url(value): - errors.append(f"{name} must be a valid http(s) URL") + errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host") - if not values["VITE_POSTHOG_KEY"]: - errors.append("VITE_POSTHOG_KEY must be set for release builds") + if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS: + errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder") if errors: print("Telemetry env validation failed:", file=sys.stderr) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 673e9c13..c907a642 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -195,9 +195,22 @@ jobs: run: | python3 <<'PY' import os + import re import sys from urllib.parse import urlparse + DISALLOWED_PLACEHOLDERS = { + "", + "-", + "_", + "false", + "true", + "null", + "undefined", + "none", + "disabled", + } + def normalize(name: str) -> str: value = os.getenv(name, "").strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'): @@ -209,9 +222,28 @@ jobs: return value return f"https://{value}" + def normalize_hostname(hostname: str) -> str: + normalized = hostname.strip().rstrip('.').lower() + if normalized.startswith('[') and normalized.endswith(']'): + normalized = normalized[1:-1] + return normalized + + def is_ip_address(hostname: str) -> bool: + if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}", hostname): + return all(0 <= int(part) <= 255 for part in hostname.split('.')) + return ':' in hostname and re.fullmatch(r"[\da-f:]+", hostname, re.IGNORECASE) is not None + + def is_allowed_hostname(hostname: str) -> bool: + normalized = normalize_hostname(hostname) + if not normalized or normalized in DISALLOWED_PLACEHOLDERS: + return False + if normalized == 'localhost': + return True + return '.' in normalized or is_ip_address(normalized) + def is_http_url(value: str) -> bool: parsed = urlparse(normalize_http_like(value)) - return parsed.scheme in {"http", "https"} and bool(parsed.netloc) + return parsed.scheme in {"http", "https"} and is_allowed_hostname(parsed.hostname or "") values = { name: normalize(name) @@ -226,13 +258,13 @@ jobs: for name in ("VITE_SENTRY_DSN", "SENTRY_DSN", "VITE_POSTHOG_HOST"): value = values[name] - if not value: - errors.append(f"{name} must be set for release builds") + if value.lower() in DISALLOWED_PLACEHOLDERS: + errors.append(f"{name} must be set to a real value, not a placeholder") elif not is_http_url(value): - errors.append(f"{name} must be a valid http(s) URL") + errors.append(f"{name} must be a valid http(s) URL with a non-placeholder host") - if not values["VITE_POSTHOG_KEY"]: - errors.append("VITE_POSTHOG_KEY must be set for release builds") + if values["VITE_POSTHOG_KEY"].lower() in DISALLOWED_PLACEHOLDERS: + errors.append("VITE_POSTHOG_KEY must be set to a real project API key, not a placeholder") if errors: print("Telemetry env validation failed:", file=sys.stderr)