{"record":{"id":"a6bc60b433dee501","repo":"github/spec-kit","slug":"providers-i-invalid-host-pattern-h-r-only-e","errorCode":null,"errorMessage":"providers[{i}]: invalid host pattern {h!r}. Only exact hostnames or '*.suffix' forms are allowed (e.g. 'github.com' or '*.visualstudio.com').","messagePattern":"providers\\[(.+?)\\]: invalid host pattern (.+?)\\. Only exact hostnames or '\\*\\.suffix' forms are allowed \\(e\\.g\\. 'github\\.com' or '\\*\\.visualstudio\\.com'\\)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/authentication/config.py","lineNumber":139,"sourceCode":"    if not isinstance(providers_raw, list):\n        raise ValueError(\"auth.json must contain a 'providers' array\")\n\n    entries: list[AuthConfigEntry] = []\n    for i, entry_raw in enumerate(providers_raw):\n        if not isinstance(entry_raw, dict):\n            raise ValueError(f\"providers[{i}]: must be a JSON object\")\n\n        hosts = entry_raw.get(\"hosts\")\n        if not isinstance(hosts, list) or not hosts:\n            raise ValueError(f\"providers[{i}]: 'hosts' must be a non-empty array\")\n        if not all(isinstance(h, str) and h.strip() for h in hosts):\n            raise ValueError(f\"providers[{i}]: each host must be a non-empty string\")\n        # Normalize hosts: strip whitespace and lowercase\n        hosts = [h.strip().lower() for h in hosts]\n        # Reject dangerous wildcard forms (e.g. *github.com matches github.com.evil.com)\n        for h in hosts:\n            if not _is_valid_host_pattern(h):\n                raise ValueError(\n                    f\"providers[{i}]: invalid host pattern {h!r}. \"\n                    \"Only exact hostnames or '*.suffix' forms are allowed \"\n                    \"(e.g. 'github.com' or '*.visualstudio.com').\"\n                )\n\n        provider = entry_raw.get(\"provider\", \"\")\n        if not isinstance(provider, str) or not provider:\n            raise ValueError(f\"providers[{i}]: 'provider' must be a non-empty string\")\n\n        auth = entry_raw.get(\"auth\", \"\")\n        if not isinstance(auth, str) or not auth:\n            raise ValueError(f\"providers[{i}]: 'auth' must be a non-empty string\")\n\n        token = entry_raw.get(\"token\")\n        token_env = entry_raw.get(\"token_env\")\n\n        # Validate token/token_env types\n        if token is not None and (not isinstance(token, str) or not token.strip()):","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/authentication/config.py#L121-L157","documentation":"Raised while parsing the `providers` array in the auth config when a `hosts` entry fails the `_is_valid_host_pattern` check. Only exact hostnames (e.g. `github.com`) or single-label `*.suffix` wildcards (e.g. `*.visualstudio.com`) are accepted; broader wildcards like `*github.com`, `*.`, or hosts containing scheme/path characters are rejected because they can match attacker-controlled domains (e.g. `*github.com` also matches `github.com.evil.com`).","triggerScenarios":"An entry in the `providers` JSON/YAML list contains a host string that is neither a plain hostname nor a `*.suffix` pattern — e.g. `\"*github.com\"`, `\"https://github.com\"`, `\"*\"`, `\"github.com/*\"`, or `\"*.*.com\"`. Hosts are stripped and lowercased before the check, so whitespace/case never trigger it.","commonSituations":"Copying a CORS-style or cookies-style wildcard from another tool's config; pasting a full URL instead of a hostname; trying to match all subdomains in depth with `**.` or nested wildcards.","solutions":["Use an exact hostname (e.g. `github.com`) or a single `*.suffix` wildcard (e.g. `*.visualstudio.com`)","Remove scheme/path portions from the host string — hosts are bare hostnames only","List each deeper subdomain explicitly instead of multi-level wildcards"],"exampleFix":"// before\n\"hosts\": [\"*github.com\", \"https://git.example.com\"]\n\n// after\n\"hosts\": [\"github.com\", \"git.example.com\"]","handlingStrategy":"validation","validationCode":"import re\n_HOST_EXACT = re.compile(r\"^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$\")\n\ndef is_valid_host_pattern(h: str) -> bool:\n    h = h.strip().lower()\n    if h.startswith(\"*.\"):\n        return bool(_HOST_EXACT.match(h[2:]))\n    return bool(_HOST_EXACT.match(h))","typeGuard":"def is_host_pattern_list(value: object) -> bool:\n    return (\n        isinstance(value, list)\n        and bool(value)\n        and all(isinstance(h, str) and is_valid_host_pattern(h) for h in value)\n    )","tryCatchPattern":"try:\n    load_auth_config(raw)\nexcept ValueError as exc:\n    if \"invalid host pattern\" in str(exc):\n        # surface the offending host from the message and fix the config source\n        raise SystemExit(f\"Fix auth config: {exc}\") from exc\n    raise","preventionTips":["Lint the auth config in CI with a schema check that validates hosts against exact-or-*.suffix patterns","Never paste URLs into hosts — strip scheme and path first","Remember hosts are lowercased and stripped automatically, so only structure can fail"],"tags":["authentication","config","validation","security"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}