Files
Toolbox/dev_test_scripts/integration/test_nav_auth_bypass.js
T
yangxiangyuan a3e8911deb feat: 新增作息报时器功能并替换短链接前缀为/to/
- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能
- 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题
- 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链
- 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义
- 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接
- 新增多个调试和集成测试脚本,保障数据安全和功能回归
2026-10-09 14:05:36 +08:00

115 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 导航鉴权绕过修复 · 未授权回归测试
*
* 背景(2026-09-30 发现并修复的真实漏洞):
* express.static 注册在首页鉴权路由之前,会把 GET / 当作目录请求直接返回
* public/index.html,导致九宫格首页完全绕过 nav_gate。
* 同时 /api/tools、/api/nav_bookmarks/* 也完全没鉴权,可被公网匿名读取甚至篡改。
*
* 本脚本对该漏洞做回归:所有"未授权一律拒绝"、所有"公开路径仍公开"、
* 以及"带 token 能正确握手种 cookie"都要成立。
*
* 用法(需目标服务已启动):
* node dev_test_scripts/integration/test_nav_auth_bypass.js
* node dev_test_scripts/integration/test_nav_auth_bypass.js http://localhost:8976
*
* 说明:脚本不硬编码任何 token,token 在运行时从 data/short_link.db 里读,
* 避免把真实凭据写进仓库(项目规则 13)。
*/
const path = require('path')
const Database = require('better-sqlite3')
const BASE = String(process.argv[2] || 'http://localhost:8976').replace(/\/+$/, '')
let passed = 0
let failed = 0
const check = (name, ok, detail) => {
if (ok) { passed += 1; console.log(` ✔ ${name}`) }
else { failed += 1; console.log(` ✘ ${name}${detail ? ' → ' + detail : ''}`) }
}
/** 发起请求;cookie 传 'nav_gate=1' 表示已过门禁 */
const req = async (method, urlPath, cookie) => {
const headers = {}
if (cookie) headers.Cookie = cookie
const res = await fetch(BASE + urlPath, { method, headers, redirect: 'manual' })
const text = await res.text().catch(() => '')
let json = null
try { json = JSON.parse(text) } catch (_) { /* 非 JSON */ }
return { status: res.status, text, json, location: res.headers.get('location'), setCookie: res.headers.get('set-cookie') }
}
const readTokenFromDb = () => {
try {
const db = new Database(path.join(process.cwd(), 'data', 'short_link.db'), { readonly: true })
const row = db.prepare("SELECT target_url FROM short_links WHERE target_url LIKE '%token=%' ORDER BY id DESC LIMIT 1").get()
db.close()
if (!row) return ''
const m = /[?&]token=([^&]+)/.exec(String(row.target_url || ''))
return m ? decodeURIComponent(m[1]) : ''
} catch (e) { return '' }
}
;(async () => {
console.log(`=== 导航鉴权绕过 · 未授权回归测试 ===`)
console.log(`目标:${BASE}\n`)
// ---------- 1. 修复前被绕过的路径:未授权必须全部拒绝 ----------
console.log('[1] 未授权必须拒绝(修复前这里全部是 200 泄漏)')
const root = await req('GET', '/')
check('GET / 未授权 → 401', root.status === 401, `实际 ${root.status},body ${root.text.slice(0, 60)}`)
const idx = await req('GET', '/index.html')
check('GET /index.html 未授权 → 401', idx.status === 401, `实际 ${idx.status}`)
const toolsDir = await req('GET', '/tools/')
check('GET /tools/ 未授权 → 401', toolsDir.status === 401, `实际 ${toolsDir.status}`)
const apiTools = await req('GET', '/api/tools')
check('GET /api/tools 未授权 → 401', apiTools.status === 401, `实际 ${apiTools.status},${apiTools.text.slice(0, 60)}`)
const nbState = await req('GET', '/api/nav_bookmarks/state')
check('GET /api/nav_bookmarks/state 未授权 → 401', nbState.status === 401, `实际 ${nbState.status}`)
const nbCats = await req('GET', '/api/nav_bookmarks/categories')
check('GET /api/nav_bookmarks/categories 未授权 → 401', nbCats.status === 401, `实际 ${nbCats.status}`)
const nbBooks = await req('GET', '/api/nav_bookmarks/bookmarks')
check('GET /api/nav_bookmarks/bookmarks 未授权 → 401', nbBooks.status === 401, `实际 ${nbBooks.status}`)
// 写接口必须同样被挡住(用不存在的 code 做 DELETE,即便漏挡也不会破坏真实数据)
const nbDel = await req('DELETE', '/api/nav_bookmarks/categories/__auth_probe_not_exist__')
check('DELETE /api/nav_bookmarks/categories/* 未授权 → 401', nbDel.status === 401, `实际 ${nbDel.status}`)
// ---------- 2. 公开路径必须保持公开 ----------
console.log('\n[2] 公开路径保持公开(不能误伤)')
const shortLink = await req('GET', '/to/__no_such_code__')
check('GET /to/<不存在的短码> → 404(而不是 401,说明短链跳转层未被门禁挡)',
shortLink.status === 404, `实际 ${shortLink.status},${shortLink.text.slice(0, 40)}`)
const legacyShortLink = await req('GET', '/l/__no_such_code__')
check('GET /l/<不存在的短码> → 404(旧前缀仍公开可用)',
legacyShortLink.status === 404, `实际 ${legacyShortLink.status}`)
// ---------- 3. 带 token 的握手必须真正种下 cookie ----------
console.log('\n[3] token 握手(修复前被 static 吞掉、cookie 永远种不下)')
const token = readTokenFromDb()
if (!token) {
console.log(' ⚠ data/short_link.db 里没找到带 token 的短链接,跳过握手测试')
} else {
const hs = await req('GET', '/?token=' + encodeURIComponent(token))
check('GET /?token=<有效> → 302(进入握手流程,而非被 static 直接返页面)',
hs.status === 302, `实际 ${hs.status},body ${hs.text.slice(0, 60)}`)
check('握手响应种下 nav_gate cookie',
!!(hs.setCookie && hs.setCookie.indexOf('nav_gate=1') >= 0), String(hs.setCookie || '').slice(0, 80))
const bad = await req('GET', '/?token=not_a_valid_jwt')
check('GET /?token=<无效> → 401', bad.status === 401, `实际 ${bad.status}`)
}
// ---------- 4. 已过门禁(带 nav_gate)时必须正常放行 ----------
console.log('\n[4] 带 nav_gate 时必须正常放行')
const okRoot = await req('GET', '/', 'nav_gate=1')
check('GET / 带门禁 → 200', okRoot.status === 200, `实际 ${okRoot.status}`)
check('GET / 带门禁返回的是九宫格首页', okRoot.text.indexOf("Yang's Toolbox") >= 0, okRoot.text.slice(0, 60))
const okTools = await req('GET', '/api/tools', 'nav_gate=1')
check('GET /api/tools 带门禁 → 200', okTools.status === 200, `实际 ${okTools.status}`)
const okNb = await req('GET', '/api/nav_bookmarks/state', 'nav_gate=1')
check('GET /api/nav_bookmarks/state 带门禁 → 200', okNb.status === 200, `实际 ${okNb.status}`)
console.log(`\n=== 结果:通过 ${passed} 项,失败 ${failed} 项 ===`)
process.exit(failed === 0 ? 0 : 1)
})()