---
title: "回跳地址通过了前缀检查：用 URL 解析结果校验来源"
date: 2026-11-13
slug: url-origin-allowlist
canonical: https://kernelranch.com/blog/2026/11/url-origin-allowlist
markdown_url: https://kernelranch.com/blog/2026/11/url-origin-allowlist.md
tags: ["javascript", "url", "security"]
---登录后跳回 return_to 指定地址。用字符串前缀检查 https://app.example.com 时，域名后缀和 URL 用户信息都可能改变浏览器实际访问的主机。先按 URL 标准解析，再比较解析后的 origin，并把审核过的序列化结果交给重定向。

## 1. 用反例测试允许列表

保存为 origin_lab.mjs，运行 node origin_lab.mjs。所有输入仅用于解析。

```javascript
import assert from 'node:assert/strict';

const app = new URL('https://app.example.com/');
function validatedTarget(input) {
  let url;
  try {
    url = new URL(input, app);
  } catch {
    return null;
  }
  if (url.protocol !== 'https:' || url.origin !== app.origin ||
      url.username || url.password) {
    return null;
  }
  return url.href;
}

const cases = [
  ['/orders', 'https://app.example.com/orders'],
  ['https://APP.example.com:443/orders', 'https://app.example.com/orders'],
  ['https://app.example.com.evil.example/orders', null],
  ['https://app.example.com@evil.example/orders', null],
  ['//evil.example/orders', null],
  ['https://app.example.com:8443/orders', null],
  ['javascript:alert(1)', null],
  ['blob:https://app.example.com/id', null],
];
for (const [value, expected] of cases) {
  const target = validatedTarget(value);
  assert.equal(target, expected);
  console.log(value, '=>', target ?? 'rejected');
}
assert.equal(cases[2][0].startsWith(app.origin), true);
assert.equal(cases[3][0].startsWith(app.origin), true);
```

本机实测输出如下。

```text
/orders => https://app.example.com/orders
https://APP.example.com:443/orders => https://app.example.com/orders
https://app.example.com.evil.example/orders => rejected
https://app.example.com@evil.example/orders => rejected
//evil.example/orders => rejected
https://app.example.com:8443/orders => rejected
javascript:alert(1) => rejected
blob:https://app.example.com/id => rejected
```

## 2. 区分来源限制和页面限制

origin 包含协议、主机和端口，默认端口经过规范化。只比较 hostname 会同时放过使用其他协议或非默认端口的目标。本例额外限制协议，避免 origin 继承自内部 URL 的 blob 地址通过。用户信息段也被明确拒绝。

如果登录回跳只能去少数业务页面，可以直接接受一个路由名，从服务端固定表生成目标地址。必须支持 URL 时，在解析后比较 origin，再校验允许的路径。只校验来源仍会允许同源站点上的其他路径，包括可继续重定向的入口。

## 小结

校验与实际重定向共用一次 URL 解析。测试域名后缀、用户信息、协议相对地址与非默认端口，保存通过检查的序列化 URL。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| WHATWG URL | https://url.spec.whatwg.org/#url-parsing | URL 解析与规范化 |
| WHATWG URL | https://url.spec.whatwg.org/#origin | origin 的计算和 blob 行为 |

## 核验说明

- Node.js v24.19.0 验证 URL 解析和校验结果，未访问示例地址。
