fix: execute canonical browser download targets
Replace the boolean browser-managed target validator with resolveBrowserManagedTarget, which returns the parsed canonical absolute URL, and hand that exact value to the host. Previously the raw href was passed on, so a relative target was re-resolved against document.baseURI and a hostile <base> could send the navigation to an origin the policy never approved. STO-08 stays UNVERIFIED: the capability spec does not exercise the system picker, so the receiver-binding hypothesis is neither reproduced nor refuted and no source change was made for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
618da9abf5
commit
ba79060a83
@@ -443,20 +443,23 @@ function browserManagedHandoff(context: Readonly<{
|
||||
context.options.observer,
|
||||
);
|
||||
}
|
||||
const href = capability.value.href;
|
||||
if (
|
||||
!safeBrowserManagedTarget(href, context.baseOrigin, {
|
||||
const target = resolveBrowserManagedTarget(
|
||||
capability.value.href,
|
||||
context.baseOrigin,
|
||||
{
|
||||
allowCrossOrigin:
|
||||
context.options.allowCrossOriginBrowserHandoff ?? false,
|
||||
allowQuery: context.options.allowBrowserManagedQuery ?? false,
|
||||
})
|
||||
) {
|
||||
return observeResult(
|
||||
browserDataFailure("POLICY_REJECTED", "DOWNLOAD"),
|
||||
context.options.observer,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (!target.ok) {
|
||||
return observeResult(target, context.options.observer);
|
||||
}
|
||||
context.options.host.handoff(href, context.suggestedFileName);
|
||||
// The host receives the parsed canonical URL, never the raw string.
|
||||
context.options.host.handoff(
|
||||
target.value.absoluteHref,
|
||||
context.suggestedFileName,
|
||||
);
|
||||
return observeResult(
|
||||
browserDataSuccess(
|
||||
Object.freeze({
|
||||
@@ -1245,28 +1248,45 @@ function validateBrowserManagedCapability(
|
||||
);
|
||||
}
|
||||
|
||||
function safeBrowserManagedTarget(
|
||||
type ResolvedBrowserManagedTarget = Readonly<{ absoluteHref: string }>;
|
||||
|
||||
/**
|
||||
* STO-02. Parse once, canonicalize, then execute the canonical value.
|
||||
*
|
||||
* Returning a boolean and handing the raw href to the host let the browser
|
||||
* re-resolve a relative target against `document.baseURI`, so a hostile
|
||||
* `<base>` could send the navigation to a different origin than the one this
|
||||
* policy just approved.
|
||||
*/
|
||||
function resolveBrowserManagedTarget(
|
||||
href: string,
|
||||
baseOrigin: string,
|
||||
policy: Readonly<{
|
||||
allowCrossOrigin: boolean;
|
||||
allowQuery: boolean;
|
||||
}>,
|
||||
): boolean {
|
||||
): BrowserDataResult<ResolvedBrowserManagedTarget> {
|
||||
let base: URL;
|
||||
let target: URL;
|
||||
try {
|
||||
const base = new URL(baseOrigin);
|
||||
const target = new URL(href, base);
|
||||
return (
|
||||
["http:", "https:"].includes(target.protocol) &&
|
||||
target.username.length === 0 &&
|
||||
target.password.length === 0 &&
|
||||
(policy.allowCrossOrigin || target.origin === base.origin) &&
|
||||
(policy.allowQuery || target.search.length === 0) &&
|
||||
target.hash.length === 0
|
||||
);
|
||||
base = new URL(baseOrigin);
|
||||
target = new URL(href, base);
|
||||
} catch {
|
||||
return false;
|
||||
return browserDataFailure("POLICY_REJECTED", "DOWNLOAD");
|
||||
}
|
||||
if (
|
||||
!["http:", "https:"].includes(target.protocol) ||
|
||||
target.username.length > 0 ||
|
||||
target.password.length > 0 ||
|
||||
(!policy.allowCrossOrigin && target.origin !== base.origin) ||
|
||||
(!policy.allowQuery && target.search.length > 0) ||
|
||||
target.hash.length > 0
|
||||
) {
|
||||
return browserDataFailure("POLICY_REJECTED", "DOWNLOAD");
|
||||
}
|
||||
return browserDataSuccess(
|
||||
Object.freeze({ absoluteHref: target.href }),
|
||||
);
|
||||
}
|
||||
|
||||
function safeOpaqueId(value: unknown): value is string {
|
||||
|
||||
Reference in New Issue
Block a user