fix(vault): bind manager logins to every saved web origin

1Password/Bitwarden items can carry several websites, but both backends
collapsed the item's urls[]/uris[] to the first origin that normalizes,
so browser_vault_fill refused every other explicitly saved origin with
origin_mismatch. Reordering the URLs in the manager just moved which
single origin worked.

VaultItemMeta now carries allowed_origins (every normalized, deduped
web origin; origin stays the first/primary one). Fill matching stays
exact-origin against that list — no wildcard, parent-domain or subdomain
inference — and the in-page synchronous check pins the origin actually
matched via build_fill_js(expected_origin=page_origin). App URIs such as
androidapp:// never widen the fill set.
This commit is contained in:
liuhao1024
2026-09-14 08:31:04 +08:00
committed by Teknium
parent ddd084a24c
commit 5fff41e52e
6 changed files with 194 additions and 21 deletions
+18 -7
View File
@@ -229,6 +229,8 @@ def browser_vault_list() -> str:
for meta in metas:
entry = {"handle": meta.id, "backend": backend.name, "label": meta.label, "kind": meta.kind,
"origin": meta.origin, "available": meta.kind == "login" or bool(meta.origin)}
if len(meta.allowed_origins) > 1:
entry["allowed_origins"] = list(meta.allowed_origins)
if meta.has_otp or backend.needs_unlock:
entry["two_factor"] = "automatic" if meta.has_otp else "automatic if the manager stores a TOTP seed, else the user is asked"
if meta.identifier:
@@ -438,20 +440,29 @@ def browser_vault_fill(handle: str, task_id: Optional[str] = None) -> str:
# ── Origin binding pre-check (cheap early exit; the authoritative check
# runs synchronously inside the fill script itself) ──────────────────────
page_origin = _focus_bound_origin(effective_task_id, str(meta.origin), meta.kind) or _current_page_origin(effective_task_id)
# Manager items can bind several websites (e.g. amazon.co.uk + www.amazon.co.uk);
# every saved origin is a valid fill target. Matching stays exact-origin —
# nothing wildcard/parent-domain is ever inferred.
allowed = list(meta.allowed_origins) or ([str(meta.origin)] if meta.origin else [])
page_origin = None
for candidate in allowed:
page_origin = _focus_bound_origin(effective_task_id, candidate, meta.kind)
if page_origin:
break
page_origin = page_origin or _current_page_origin(effective_task_id)
if not page_origin:
return json.dumps(
{"success": False, "error": "Could not determine the current page origin. Navigate to the login page first."}
)
if page_origin != meta.origin:
if page_origin not in allowed:
return json.dumps(
{
"success": False,
"error_type": "origin_mismatch",
"error": (
f"Refused: current page origin ({page_origin}) does not match "
f"the vault item's bound origin ({meta.origin}). Vault fills "
"only run on the exact origin the credential was saved for."
f"the vault item's bound origin(s) ({', '.join(allowed)}). Vault fills "
"only run on the exact origin(s) the credential was saved for."
),
}
)
@@ -505,7 +516,7 @@ def browser_vault_fill(handle: str, task_id: Optional[str] = None) -> str:
try:
fill_result = _eval_js_secret(
effective_task_id, build_fill_js(fills, expected_origin=str(meta.origin), nonce=nonce)
effective_task_id, build_fill_js(fills, expected_origin=page_origin, nonce=nonce)
)
except Exception as exc:
# Strip any secret material from exception text before surfacing.
@@ -529,7 +540,7 @@ def browser_vault_fill(handle: str, task_id: Optional[str] = None) -> str:
"error_type": "origin_changed",
"error": (
"Refused: the page navigated away from the bound origin "
f"({meta.origin}) before the fill could run "
f"({page_origin}) before the fill could run "
f"(now on {parsed.get('found') or 'unknown'}). "
"Nothing was written."
),
@@ -538,7 +549,7 @@ def browser_vault_fill(handle: str, task_id: Optional[str] = None) -> str:
filled = parsed.get("filled", 0) if isinstance(parsed, dict) else 0
out = {"success": bool(filled), "filled_fields": int(filled), "backend": backend.name,
"kind": meta.kind, "origin": meta.origin}
"kind": meta.kind, "origin": page_origin}
if meta.kind == "login":
out["next"] = ("Submit. If the site then asks for a verification code, call browser_vault_enter_code with this handle"
+ (" (a code will be generated automatically)." if meta.has_otp else "."))