fix(core): 行尾句读不再让真 token 整串落盘(M243 BEARERDOT)
`Bearer <JWT>.`(行尾一个英文句点)整串原样落盘:句点自己属于 dotted 字符集,
扫描器把它算进 run 后 `split('.')` 多切出一个空段,「每段 ≥8」当场不成立。
M240 实测 old == new == 原文,影响 core 的全部落盘/展示面。
修法不是抬阈值:`shape::split_trailing_punct` 在判定前把末尾句读(`.` `,` `;` `)`)
切出去,`push_dotted` 再把句读原位写回——占位符只吃 token 本体,句子标点一个不丢。
只有 `.` 会进入字符集,其余三个本来就在外面,列全是为了「句读不参与判定」只有一处出处。
`has_dotted_token` 的闸门同步改成看同一份切分结果,否则闸门关着就静默不抹。
判据:`tests/bearerdot.rs` 三组——四种形状(`Bearer <JWT>.` / `… <JWT>. then` /
`<JWT>,` / `(<JWT>)`)必须抹且标点保留;句点结尾的普通文本(`Service.Unavailable.today.`)
必须逐字原样;两个入口在带句读的输入上必须一致。语料 68 文件 / 243 行仍 0 行被改,
JWT/sha256/base64 正例仍被抹。变异自证:把 trim 改回旧行为 → 主判据红(EXIT=101)。
外层两个既有红(diagnose::session / presenter m241 夹具)是 M240 收窄后的口径冲突,
stash 回 main HEAD 可复现,已各 file 一条 finding,不在本轮修。
This commit is contained in:
1 parent
ea108c5fc4
commit
cc90c64b84
4 files changed
+140
-4
No files matched your search
@@ -28,6 +28,15 @@
|
||||
//! base64 的正例全部仍被抹;但有三类形状是「旧行为抹、新行为留」——`=` 边界、填充不计段长、
|
||||
//! 全小写点分串。三条都在 [`shape`] 的「旧行为抹、新行为留」一节里逐条写明理由,
|
||||
//! 前两条是本次要修的误杀,第三条是代价。
|
||||
//!
|
||||
//! # 句读不是 token 本体(M243 BEARERDOT)
|
||||
//!
|
||||
//! `Bearer eyJ….eyJ….c2ln….`(行尾一个英文句点)在 M240 之后仍整串原样落盘:句点自己属于
|
||||
//! dotted 字符集,被算进 run 后 `split('.')` 多切出一个空段,「每段 ≥8」当场不成立。修法同样
|
||||
//! 不是抬阈值——判定前把**末尾**句读切出去([`shape::split_trailing_punct`]),搬运方
|
||||
//! ([`push_dotted`])再把句读原位写回,所以占位符只吃 token 本体,句子标点一个不丢。
|
||||
//! 只削末尾:中间的句点仍是 token 的一部分(JWT 的三段就是靠它切的);`(`、`)`、`,`、`;`
|
||||
//! 本来就在字符集外,一并列进规则是为了「句读不参与判定」这件事只有一个出处。
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
@@ -37,7 +46,8 @@ use serde_json::Value;
|
||||
mod shape;
|
||||
|
||||
use self::shape::{
|
||||
is_dns_host, is_dotted_token, is_dotted_token_char, is_opaque_char, is_path_run, MAX_OPAQUE_RUN,
|
||||
is_dns_host, is_dotted_token, is_dotted_token_char, is_opaque_char, is_path_run,
|
||||
split_trailing_punct, MAX_OPAQUE_RUN,
|
||||
};
|
||||
|
||||
/// access token / session token 的替换标记。
|
||||
@@ -238,7 +248,7 @@ pub fn has_dotted_token(text: &str) -> bool {
|
||||
fn run_has_dotted_token(run: &str) -> bool {
|
||||
match split_authority(run) {
|
||||
Some((_, rest)) => run_has_dotted_token(rest),
|
||||
None => is_dotted_token(run),
|
||||
None => is_dotted_token(split_trailing_punct(run).0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,11 +280,14 @@ fn push_dotted(out: &mut String, run: &str) {
|
||||
push_dotted(out, rest);
|
||||
return;
|
||||
}
|
||||
if is_dotted_token(run) {
|
||||
// 句读不参与判定,但要原位保留:`Bearer <JWT>.` 的句点不许被占位符吃掉。
|
||||
let (body, punct) = split_trailing_punct(run);
|
||||
if is_dotted_token(body) {
|
||||
out.push_str(TOKEN_PLACEHOLDER);
|
||||
} else {
|
||||
out.push_str(run);
|
||||
out.push_str(body);
|
||||
}
|
||||
out.push_str(punct);
|
||||
}
|
||||
|
||||
/// 把 URL 形状的 run 切成(主机名前缀, 路径那半)。路径那半不含分隔用的那个 `/`。
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
//! 四条里前两条是**位置/字符集**判据,后两条是**形状**判据;都写成必要条件:判据不成立时
|
||||
//! 仍然照抹,只有"这一串根本不可能是凭据"时才放行。
|
||||
//!
|
||||
//! 第五处边界(M243 BEARERDOT,同样不是抬阈值):**末尾句读不参与判定**
|
||||
//! ([`split_trailing_punct`])。判定前把 `.` `,` `;` `)` 切出去,搬运方原位写回——
|
||||
//! 行尾句点原先会多切出一个空段,把真 token 整串放行。只有 `.` 会进入 dotted 字符集,
|
||||
//! 其余三个本来就在外面;列全是为了「句读不参与判定」只有一处出处。
|
||||
//!
|
||||
//! 关于"必须含大写"的实际覆盖面(复审 P2-2 的怀疑成立一半):**DNS 形状**的全小写点分串
|
||||
//! 早在首段 DNS 豁免那一条就被救下了,跟大写判据无关;大写判据唯一扛住的是**不是 DNS 形状**
|
||||
//! 的全小写点分串(标签带 `_`、标签超 63 字符、`/` 前只有一段主机名)。它保留下来,因为那类
|
||||
@@ -75,6 +80,21 @@ pub(super) fn is_dotted_token(run: &str) -> bool {
|
||||
jwt_shaped(run) && !is_url_authority(run)
|
||||
}
|
||||
|
||||
/// 把 run 末尾的句读切出去:返回(判据只看的前半, 原样搬回的后半)。
|
||||
///
|
||||
/// 句读(`.` `,` `;` `)`)不属于 token 本体。`.` 是**唯一会进入** dotted 字符集的句读
|
||||
/// (见 [`is_dotted_token_char`]),所以行尾一个句点就会把 run 多切出一个空段:
|
||||
/// 「每段 ≥8」当场不成立,整串原样落盘——BEARERDOT(M243)。`(` `,` `;` 早被字符集
|
||||
/// 切在 run 外,这里一并列出,是把「句读不参与判定」写成一条规则:字符集将来若放宽,
|
||||
/// 判据不用再改一遍。
|
||||
///
|
||||
/// 只削**末尾**:中间的句点仍是 token 本体(JWT 的三段就是靠它切的)。
|
||||
/// 削短只会让形状判据更容易成立,方向在安全那一侧;搬运方负责把切出去的后半原位写回。
|
||||
pub(super) fn split_trailing_punct(run: &str) -> (&str, &str) {
|
||||
let body = run.trim_end_matches(['.', ',', ';', ')']);
|
||||
run.split_at(body.len())
|
||||
}
|
||||
|
||||
/// 「带点 token」的形状:段数 ≥3、每段 ≥8 字节、总长 ≥40(老规则,一个字都没放宽),
|
||||
/// 外加两条必要条件——首段不是主机名、**含大写字母**。
|
||||
///
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
//! 三组:**正例**(必须抹,占位符形状固定)、**误杀**(M239 实测的形状,必须逐字原样)、
|
||||
//! **边界**(新判据最容易被拼出来的形状,以及 `has_*` / `mask_*` 两个入口的一致性)。
|
||||
//! 语料组拿 M239 的回归语料(68 文件 / 243 行)当钉死的输入,要求一行都不许改。
|
||||
//!
|
||||
//! BEARERDOT(token 后面跟句读,M243)单独一页:`tests/bearerdot.rs`。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
|
||||
// `tests.rs` 是以 `#[path]` 挂进来的文件模块,子模块按**它所在目录**(`redact/`)解析,
|
||||
// 所以这里写全相对路径,文件仍归在 `tests/` 目录下。
|
||||
#[path = "tests/bearerdot.rs"]
|
||||
mod bearerdot;
|
||||
|
||||
/// 真实形状的 JWT(`header.payload.signature`,三段各 ≥8)。
|
||||
const JWT: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.c2lnbmF0dXJlLXZhbHVlLXRoYXQtaXMtbG9uZy1lbm91Z2g";
|
||||
/// 微软 MSA/XSTS 形状的 header(`{"typ":"JWT","alg":"none"}`)。
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//! BEARERDOT(M243):token 后面跟句读时也要抹,句读原位保留。
|
||||
//!
|
||||
//! # 漏抹的形状
|
||||
//!
|
||||
//! `Bearer eyJ….eyJ….c2ln….`(行尾一个英文句点):句点自己属于 dotted 字符集
|
||||
//! ([`is_dotted_token_char`]),扫描器把它算进 run,`split('.')` 多切出一个空段,
|
||||
//! 「每段 ≥8」当场不成立,整串原样落盘。M240 实测 `old == new == 原文`,
|
||||
//! 影响面是 core 的全部落盘/展示面(会话 JSONL、`--dry-run`、诊断规则输入)。
|
||||
//!
|
||||
//! # 修法
|
||||
//!
|
||||
//! 不是抬阈值,是**判定前把句读切出去**([`split_trailing_punct`]):判据只看 token 本体,
|
||||
//! 句读由搬运方(`push_dotted`)原位写回。方向是对安全的那一侧:削短只会让形状判据
|
||||
//! 更容易成立,不会把真 token 放过去。`(`、`)`、`,`、`;` 早就在字符集外,一并钉住,
|
||||
//! 免得下次改字符集时又把这四个位置漏掉。
|
||||
//!
|
||||
//! # 另一半:句读不是「见到句点就抹」的入口
|
||||
//!
|
||||
//! 削句读之后必须证明句点分隔的普通文本没被顺手拖进来(`Service.Unavailable.today.`),
|
||||
//! 这一侧由 [`sentences_ending_in_punctuation_still_survive`] 钉住;否则修法等于用
|
||||
//! 「行尾有没有句点」当抹除判据,那是把 M239/M240 修掉的误杀换了个形状请回来。
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 真凭据 + 句读:token 照抹,句读原位保留(不是被占位符吃掉,也不是被当噪声丢掉)。
|
||||
#[test]
|
||||
fn trailing_sentence_punctuation_does_not_hide_a_token() {
|
||||
let cases = [
|
||||
// form ①:行尾句点
|
||||
(
|
||||
format!("Bearer {JWT}."),
|
||||
format!("Bearer {TOKEN_PLACEHOLDER}."),
|
||||
),
|
||||
// form ②:句点后还有正文(`"…c2ln…. then"`)
|
||||
(
|
||||
format!("the token was {JWT}. then"),
|
||||
format!("the token was {TOKEN_PLACEHOLDER}. then"),
|
||||
),
|
||||
// form ③:逗号
|
||||
(format!("{JWT},"), format!("{TOKEN_PLACEHOLDER},")),
|
||||
// form ④:括号(左右都在字符集外,钉住说明它们从来不是判据的一部分)
|
||||
(format!("({JWT})"), format!("({TOKEN_PLACEHOLDER})")),
|
||||
// MSA/XSTS 票据被抄进日志时正是这个位置:键 + 分号
|
||||
(
|
||||
format!("xsts: {MSA};"),
|
||||
format!("xsts: {TOKEN_PLACEHOLDER};"),
|
||||
),
|
||||
// URL 路径里的 token + 行尾句点(主机名豁免只管主机名那半)
|
||||
(
|
||||
format!("https://cdn.example.com/{JWT}."),
|
||||
format!("https://cdn.example.com/{TOKEN_PLACEHOLDER}."),
|
||||
),
|
||||
];
|
||||
for (input, expected) in cases {
|
||||
assert_eq!(scrub(&input), expected, "输入:{input}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 句点分隔的普通文本:削句读不许把「行尾有句点」变成新的抹除入口。
|
||||
///
|
||||
/// 每一条都是 `brand_and_property_strings_survive` / `long_paths_and_class_paths_survive`
|
||||
/// 里已经钉过的形状,末尾加一个句点——断言 `scrub` 逐字原样,且闸门 `has_dotted_token` 不开。
|
||||
#[test]
|
||||
fn sentences_ending_in_punctuation_still_survive() {
|
||||
for text in [
|
||||
"Service.Unavailable.today.",
|
||||
"the error came back. Service.Unavailable.today.",
|
||||
"sophisticatedbackpacks.upgrades.reference.",
|
||||
"xbl.xsts.entitlements.profile.",
|
||||
"Java Path: /home/alice/.minecraft/runtime/java-runtime-delta/bin/java.",
|
||||
] {
|
||||
assert!(!has_dotted_token(text), "闸门开了:{text}");
|
||||
assert_eq!(scrub(text), text, "误杀:{text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 两个入口在带句读的输入上仍要一致:`has_*` 开着闸门,`mask_*` 就必须真的改。
|
||||
///
|
||||
/// `scrub` 用 `has_dotted_token` 当闸门,闸门与搬运方对句读的看法一旦分叉(一边削、一边不削),
|
||||
/// `scrub` 会静默走「闸门关着 → 什么都不抹」这条路,测试却全绿。
|
||||
#[test]
|
||||
fn the_has_gate_agrees_on_trailing_punctuation() {
|
||||
for case in [
|
||||
format!("Bearer {JWT}."),
|
||||
format!("{JWT},"),
|
||||
format!("({JWT})"),
|
||||
format!("the token was {MSA}. then"),
|
||||
"Service.Unavailable.today.".to_string(),
|
||||
] {
|
||||
assert_eq!(
|
||||
has_dotted_token(&case),
|
||||
mask_dotted_tokens(&case) != case,
|
||||
"has_dotted_token 与 mask_dotted_tokens 不一致:{case}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user