PTA 的"代码框禁止粘贴"

顺手扒了一下 PTA 的"代码框禁止粘贴"

起因

某天写 PTA 上的题,习惯性地把 IDE 里调好的代码 Ctrl+V 进去,结果右下角弹出一行灰色提示:

本题目集禁用代码粘贴

代码一个字都没进去。

我承认这功能确实是教师用来防作弊的,本意没错。但作为一个学信息安全的大一学生,遇到这种"前端拦你一下"的场景,第一反应不是骂街,而是——它是怎么拦的?能不能扒开看看?

写这篇博客主要是把整个分析过程记下来,方法论比结论本身值钱得多。

⚠️ 先把丑话说前面:考试场景下绕过这个功能是学术不端,被抓后果你懂的。本文目的是练习前端逆向方法,别拿去考试作弊。

一、先看看手头有什么

打开 PTA 题目页,F12 → Network 面板刷新一下,挑出几个看起来是 JS chunk 的文件下载下来。文件名长这样:

vendor.013ad26192205d4653a6.chunk.js   (4 MB)
main.d2b95cfb2969e684ab43.js           (2 KB)
7f6c4948adc473039877.chunk.js          (1.5 MB)

这些名字其实在告诉你它们是干嘛的。Webpack 这类打包工具会把代码切成几块:

  • vendor chunk:第三方库(React、lodash、各种 UI 组件)
  • main chunk:应用入口
  • async chunk:路由级懒加载,名字往往就是一串哈希

文件名里那一长串十六进制是 content hash,做缓存破坏用的——内容一变文件名就变,浏览器自然会去拉新版。

我先翻了 vendor 那个 4MB 的大块头,搜了一圈 paste、copy、preventDefault、粘贴、禁止,啥也没找到。中文关键词 0 匹配,事件监听也没有阻止默认行为的痕迹。这说明 vendor chunk 里只有 React、CodeMirror、ProseMirror 这些库本身,没有 PTA 的业务代码。

把矛头转向那个 1.5MB 的 async chunk(名字纯哈希的那个)。

二、字符串搜索的小坑

按理说一个面向中国学生的反作弊提示,必然有中文。我搜"粘贴"、"禁止"、"复制"——又是 0 匹配。

愣了一下我才反应过来:minifier 默认会把所有非 ASCII 字符转成 \uXXXX 转义序列。这是 Terser、UglifyJS 之类压缩工具的标准行为,目的是让文件用纯 ASCII 编码也能安全传输。

也就是说源码里的:

"本题目集禁用代码粘贴"

在压缩后会变成:

"\u672c\u9898\u76ee\u96c6\u7981\u7528\u4ee3\u7801\u7c98\u8d34"

所以搜中文要先把中文转成 Unicode 转义形式。在线找个 "Unicode 转义"工具,或者随手一段 Python:

"".join(f"\\u{ord(c):04x}" for c in "粘贴")
# '\\u7c98\\u8d34'

带着 \\u7c98\\u8d34 重新搜,立刻命中:

pos 674727: ...info("本题目集禁用代码粘贴")
pos 1265195: forbidPastingHint:"禁止考生在代码框中使用浏览器的粘贴功能"
pos 1385936: forbidPasting:"代码框禁止粘贴"

PTA 内部把这功能叫 forbidPasting,i18n 字典都给找到了。

三、顺藤摸瓜

知道了内部命名 forbidPasting,剩下的就是顺着这条线追。直接全文搜这个词,挖出几处关键引用。

第一处:监听器(pos 674633 附近)

componentDidMount(){
  const j = this.ref.current;
  j && this.props.shouldForbidPasting &&
    j.addEventListener("code-editor:rate-limit", () => {
      We.default.info("本题目集禁用代码粘贴")
    })
}

这是个 React 组件挂载时的钩子,逻辑很清楚:如果 shouldForbidPasting 是 true,就在编辑器容器上监听一个叫 code-editor:rate-limit 的自定义事件,触发了就弹 toast 提示。

code-editor:rate-limit 这个名字很有意思——它叫"速率限制"而不是"防粘贴"。这个细节后面会解释为什么。

第二处:真正的拦截逻辑(pos 897033 附近)

let L = "";
ut.iterChanges((J, oe, ge, Pe, Ae) => { L += Ae.toString() });

if (Array.from(L.replace(/\s/g, "")).length > (this.props.maxLengthOnInput || 1/0)) {
    Be.dispatchEvent(new Event("code-editor:rate-limit", { bubbles: true }));
    (0, ue.Yw)(this.codemirror);
    (0, ue.PR)(this.codemirror);
    return;
}

看到这段我直接乐了——它根本没监听 paste 事件。

它监听的是 CodeMirror 编辑器的 transaction(事务)。每次编辑器内容变化,把这次变化里所有插入的文本拼起来,去掉空白字符算长度。如果一次性插入的字符数超过 maxLengthOnInput,就 dispatch 那个 rate-limit 事件,然后调两个还原函数把编辑器状态回滚,并且 return 让本次变化不写入。

这个设计为什么聪明?

它不是针对"粘贴"这个动作,而是针对"一次性大量输入"这个结果。这意味着无论你怎么往里塞——Ctrl+V、右键粘贴、拖拽、浏览器自动填充、甚至油猴脚本程序化 setValue——只要一次塞太多就被拦。

副作用是中文输入法整段上屏也可能误伤(这其实是个绕过点,但效率极低,不值得)。

CodeMirror 6 的事务模型有点像 Git 的 commit,所有编辑都是不可变事务。updateListener 可以在事务"准备好但还没提交"的瞬间介入,所以"事后审核 + 撤销"才能做得这么干净。这套设计模式叫 CQRS 或者 Event Sourcing 的轻量版,挺值得学的。

第三处:决定生死的那一行(pos 676526)

继续追 maxLengthOnInput 这个 prop 是怎么赋值的,最后定位到:

maxLengthOnInput: Ot ? 5 : 1/0

Ot 是 shouldForbidPasting 在压缩后的别名(minifier 会把局部变量改成最短的标识符)。展开就是:

shouldForbidPasting maxLengthOnInput 行为
true(教师启用了禁止粘贴) 5 一次输入超过 5 个非空白字符就拦
false(默认) 1/0 = Infinity 永远不会触发

1/0 是个小 trick——在 JS 里这就是 Infinity,但写成 1/0 比 Infinity 这个标识符短 6 个字节,minifier 喜欢这种字面量优化。

至此整条链路全部还原:

教师勾选"代码框禁止粘贴"
        ↓
后端配置 forbidPasting: true
        ↓
前端读出来 → shouldForbidPasting prop 透传
        ↓
maxLengthOnInput 被设成 5
        ↓
你 Ctrl+V 一段代码(>5 字符)
        ↓
CodeMirror updateListener 算字符数 → 超了
        ↓
dispatch "code-editor:rate-limit" 事件 + 回滚编辑器
        ↓
toast: "本题目集禁用代码粘贴"

四、补丁方案

既然机制摸清楚了,怎么改就有几种选择:

方案 A:改阈值(最简单)

把 maxLengthOnInput:Ot?5:1/0 改成 maxLengthOnInput:1/0。

直接让上限永远是无穷大,那个 if (字符数 > 阈值) 永远进不去,撤销逻辑也就永远不执行。事件不 dispatch,监听器也不会触发,连 toast 都不会弹。一行改动解决全部问题。

方案 B:短路掉判定

把 if (...) 加个 false&& 短路:

if (false && Array.from(L.replace(/\s/g,"")).length > ...) {

逻辑等价于把整个 if 块删掉。最干净,但容易改错括号。

方案 C:拆掉监听器

把 this.props.shouldForbidPasting && j.addEventListener(...) 改成 false && j.addEventListener(...)。

但这个方案不能单独用——它只阻止 toast 弹出,撤销逻辑还在跑,粘贴的内容还是会消失。要配合方案 A 才完整。

我选方案 A,改动最少风险最小。

五、把改动应用到浏览器

文件在 PTA 服务器上,本地改的版本怎么让浏览器认?

最干净的办法是 Chrome DevTools 自带的 Local Overrides。原理是 DevTools 在浏览器网络栈里插了个拦截层:所有请求出去前先检查"这个 URL 在不在我的 Overrides 文件夹里?"——在的话直接读本地文件返回,根本不发网络请求;不在的话正常走网络。

具体步骤:

  1. F12 → Sources 面板 → 顶部 Overrides 标签(可能藏在 >> 里)
  2. Select folder for overrides,选个空文件夹,浏览器顶上会弹横幅,点"允许"
  3. 在 Sources 树里找到那个 chunk 文件,右键 → 替换内容(Override content)
  4. 文件名前会出现紫色 *,表示已被覆盖。这时文件变成可编辑状态
  5. Ctrl+F 搜 maxLengthOnInput,定位到 Ot?5:1/0,改成 1/0
  6. Ctrl+S 保存
  7. 刷新页面,保持 DevTools 打开

刷新后 Ctrl+V 一段长代码进去,没有 toast,代码完整粘进去——成了。

几个常见坑:

  • DevTools 关了就失效:Local Overrides 只在 DevTools 打开时生效,这是 Chrome 的设计。要持久化得用 Tampermonkey
  • 改完白屏:八成是改的时候碰到了别的字符。删掉本地 override 文件,重新"替换内容"再来一遍
  • 找不到 Ot?5:1/0:变量名是版本相关的随机两字母,PTA 更新后可能变成 pn、Wt 之类。直接搜 maxLengthOnInput 这一个词更稳,全文应该只有 1~2 处匹配,肉眼挑那个长得像三元表达式的就行
  • Pretty Print 干扰搜索:左下角那个 {} 按钮如果开着会把代码格式化,字符串可能被拆行。关掉再搜

六、复盘

撇开 PTA 这个具体场景,这次分析其实是一套通用流程:

  1. 区分 chunk 类型:vendor / main / async,先排除明显不是嫌疑人的
  2. 关键字粗筛:搜中英文、事件名、API 名
  3. 处理混淆:搜不到中文?想想 \uXXXX。变量名是乱码?想想 minifier 的局部变量重命名规则
  4. 找内部命名:从一个字符串("本题目集禁用代码粘贴")找到 prop 名(forbidPasting),再以这个 prop 名为锚点全文搜
  5. 顺藤摸瓜到判定语句:三元、if、&& 短路,挨个看
  6. 选补丁策略:改阈值最小、改判定中等、整段短路最彻底,按改动量从小到大试
  7. Local Overrides 应用补丁

这套流程能套到的场景远不止这一个。前端表单验证、限时按钮、新手引导、隐藏功能开关——只要逻辑跑在前端,理论上都能这么扒。当然,安全敏感的逻辑就不该放在前端,这本身就是个老生常谈的安全原则。PTA 把这个限制只放在前端,本身就说明它的目标是"让大多数人不方便",而不是"绝对防住所有人"。

七、踩过的坑和经验

写这篇时回想了下整个过程,有几个地方一开始想错了:

第一个坑是搜 paste 关键字。我最初以为肯定能搜到 addEventListener('paste', ...) 之类的代码,结果一无所获,差点以为防粘贴是用 CSS 实现的(其实 user-select: none 这种只能防选中,防不了粘贴)。后来才意识到 PTA 用的是"事后撤销"而不是"事前阻止",根本不需要监听 paste 事件。

第二个坑是没第一时间想到 Unicode 转义。这个坑现在想想很基础,但当时确实卡了我十几分钟。教训:搜 minified 文件里的中文,第一步就该转 \uXXXX。

第三个坑是 vendor chunk 浪费时间。最开始拿到的是 4MB 的 vendor chunk,硬翻了半天没结果。其实从文件名就能判断它是第三方库集合,业务代码不在里面,应该一开始就跳过。

八、能再深入的方向

如果想沿着这条路继续走,几个推荐的方向:

  • AST 反混淆:webcrack 是个不错的工具,能自动还原大部分 Webpack 产物结构。配合 Babel AST Explorer 可以肉眼读 AST
  • 抓包改包:mitmproxy / Burp Suite,比 DevTools 更底层,能在 HTTPS 层拦截一切流量。CTF-Web 标配
  • Tampermonkey 持久化补丁:Local Overrides 的进阶,可以写脚本自动 patch 任何网站
  • 真实靶场训练:PortSwigger Web Security Academy 强推,业界教材级别,免费、循序渐进、覆盖几乎所有 Web 漏洞类型

最后的最后,还是那句——会扒不代表要扒,技术中立,怎么用看你自己。

posted @ 2026-04-29 00:43  yorkchain  阅读(299)  评论(0)    收藏  举报
​