{"record":{"id":"e3b91b6de184d7a3","repo":"github/spec-kit","slug":"url-must-use-https-http-is-only-allowed-for-local","errorCode":null,"errorMessage":"URL must use HTTPS (HTTP is only allowed for localhost)","messagePattern":"URL must use HTTPS \\(HTTP is only allowed for localhost\\)","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/_commands.py","lineNumber":123,"sourceCode":"    force: bool = False,\n):\n    \"\"\"Download an archive from *url* and install it, reusing the hardened path.\n\n    Shares the same download hardening as ``extension add --from``:\n    HTTPS enforcement, the catalog's authenticated + redirect-guarded\n    ``_open_url`` fetch, a bounded (50 MiB) response read, archive-format\n    detection (ZIP or tar.gz/tgz), and a TOCTOU-safe transient download file\n    consumed directly by ``install_from_zip``.\n\n    Returns the installed manifest. Raises ``ExtensionError`` on any failure so\n    callers can present a uniform message without a second downloader.\n    \"\"\"\n    import urllib.error\n\n    from . import ExtensionCatalog, ExtensionError\n\n    if not is_https_or_localhost_http(url):\n        raise ExtensionError(\n            \"URL must use HTTPS (HTTP is only allowed for localhost)\"\n        )\n\n    download_dir = _validate_safe_cache_dir(project_root)\n    archive_filename = f\"extension-url-download-{uuid4().hex}.archive\"\n    # Only used for diagnostic messages: the real archive is a transient inode\n    # (unlinked on POSIX, O_TEMPORARY on Windows) consumed via ``archive_file``\n    # below, so this path is never opened again.\n    archive_path = download_dir / archive_filename\n\n    try:\n        dl_catalog = ExtensionCatalog(project_root)\n        download_url = url\n        extra_headers = None\n        resolved_url = dl_catalog._resolve_github_release_asset_api_url(download_url)\n        if resolved_url:\n            download_url = resolved_url\n            extra_headers = {\"Accept\": \"application/octet-stream\"}","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/_commands.py#L105-L141","documentation":"The URL installer's first gate: is_https_or_localhost_http(url) returned false, meaning the scheme is not HTTPS (and not http:// on a loopback host) — or the URL failed basic parse/shape checks. This is a deliberate security control: extension archives are executable content, so plaintext HTTP is only tolerated for localhost development.","triggerScenarios":"Calling the install-from-URL path (`specify extension install <url>` or _commands installer) with http:// on a non-localhost host, or with a scheme like file:// or ftp://.","commonSituations":"Using http:// to dodge TLS errors on an internal mirror; copy-pasting a file:// path instead of a URL; dev server on 0.0.0.0 or a LAN IP over plain http; typo dropping the s in https.","solutions":["Serve the archive over HTTPS (e.g. behind a TLS-terminating proxy or with a self-signed cert added to the trust store) and use the https:// URL","For local development, keep HTTP but address it as localhost/127.0.0.1 (::1) so the loopback exemption applies","For local files, skip the URL path entirely — install from the local archive path or directory","Fix the typo'd scheme (http:// -> https://)"],"exampleFix":"# before\nspecify extension install http://internal-mirror:8080/my-ext.zip\n\n# after (localhost exemption)\npython -m http.server 8080 &\nspecify extension install http://localhost:8080/my-ext.zip\n# or\nspecify extension install https://mirror.example.com/my-ext.zip","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef url_allowed_for_install(url: str) -> bool:\n    p = urlparse(url)\n    host = (p.hostname or '').lower()\n    return p.scheme == 'https' or (\n        p.scheme == 'http' and host in ('localhost', '127.0.0.1', '::1')\n    )\n\nassert url_allowed_for_install('https://example.com/ext.zip')","typeGuard":"def assert_installable_url(url: str) -> None:\n    if not url_allowed_for_install(url):\n        raise ValueError(\n            'Extension URLs must be https:// (http:// only on localhost)'\n        )","tryCatchPattern":null,"preventionTips":["Use https:// for all remote extension sources","For local files, pass the file path instead of a file:// URL","Use localhost (not 0.0.0.0/LAN IP) for plain-http dev mirrors"],"tags":["extensions","security","https","url-validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}