概念定义
攻击分两半:让受害者使用攻击者知道的会话 id,再让登录把这个 id 晋升为已认证。应用在权限边界上保留了 id,等于亲手交出钥匙。
工作机制
播种渠道:URL 携带会话 id、兄弟子域设的 Cookie、混合站点 HTTP 路径注入的 Set-Cookie。受害者完成认证,应用把认证绑到既有 id 上,攻击者出示同一 id 即同一会话。
使用场景
每次权限变化都轮换 id;绝不从 URL 或请求体接受 id;Cookie 用 host-only 加 __Host- 前缀收紧作用域。框架都提供会话再生 API,应在登录处显式调用而非指望默认。
常见误区
“id 够长够随机就安全”错位:熵防猜测,防不了固定——问题是攻击者登录前就知道值。购物车延续性常被拿来保留登录前 id:应把状态拷贝进新会话而不是保留 id。子域蔓延会在加固过的父域上重开 Cookie 播种通道;再生必须在服务端作废旧 id,而不只是发新 Set-Cookie。
代码示例
export function createFlagSet(names) {
if (!Array.isArray(names) || names.length === 0) {
throw new TypeError("names must be a non-empty array");
}
if (names.length > 30) throw new RangeError("at most 30 flags are supported");
const bits = new Map();
const all = [];
for (const name of names) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("flag names must be non-empty strings");
}
const normalized = name.trim();
if (bits.has(normalized)) throw new Error(`duplicate flag "${normalized}"`);
bits.set(normalized, 1 << bits.size);
all.push(normalized);
}
const resolve = (name) => {
const bit = bits.get(name);
if (bit === undefined) throw new Error(`unknown flag "${name}"`);
return bit;
};
return {
names: () => [...all],
mask(...selected) {
return selected.reduce((mask, name) => mask | resolve(name), 0);
},
has(mask, name) {
return (mask & resolve(name)) !== 0;
},
add(mask, name) {
return mask | resolve(name);
},
remove(mask, name) {
return mask & ~resolve(name);
},
toggle(mask, name) {
return mask ^ resolve(name);
},
toNames(mask) {
return all.filter((name) => (mask & resolve(name)) !== 0);
},
};
}
const permissions = createFlagSet(["read", "write", "admin"]);
let mask = permissions.mask("read", "write");
console.log(mask, permissions.toNames(mask));
mask = permissions.remove(mask, "write");
mask = permissions.toggle(mask, "admin");
console.log(permissions.has(mask, "write"), permissions.toNames(mask));
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
db.prepare('INSERT INTO users (email) VALUES (?)').run('[email protected]');
function findEmails(ids) {
if (ids.length === 0) return [];
if (ids.some((id) => !Number.isInteger(id))) {
throw new TypeError('ids must be integers');
}
const placeholders = ids.map(() => '?').join(', ');
const stmt = db.prepare(
'SELECT id, email FROM users WHERE id IN (' + placeholders + ')',
);
return stmt.all(...ids);
}
try {
// 恶意输入只是参数值,永远不会被解析成 SQL 语法
db.prepare('SELECT * FROM users WHERE email = ?').all("x' OR '1'='1");
} catch (err) {
console.log('injection attempt contained:', err instanceof Error);
}
const rows = findEmails([1, 2, 3]);
console.log('matched rows:', rows.length);
db.close();