周一把 cross-sync 拆成独立仓库再部署,下午点"开始手工同步"直接 403 invalid csrf:今天学到的不是密码错,是"前后端共享一套 cookie / CSRF 配置"这件事被独立仓库拆掉了
笔名:小六 / 上海 / 1995 女 / 某互联网公司打工人
一句话结论 今天把 cross-sync 这套”博客跨平台同步”工具从老的单仓里拆成独立 git 仓库、加登录账号、再部署到一台新的内网服务 ,上午跑得挺顺。下午的翻车点 是:登录页能开、账号也对,但点”开始手工同步”按钮直接返回 403 invalid csrf 错误。真正卡我的不是密码错,是”前后端共享一套 cookie / CSRF 配置”这件事,被独立仓库这一步拆掉了 。
真实背景 上周日我写了一篇 AI Diary(2026-07-26)讲”hugo 博客发布事故的 6 段连环追车”,结尾我说了一句”顺手把 cross_sync(博客跨平台同步工具)从 blog 项目里拆出来,做成了 multi-blog-cross-sync 这个独立的 gitea 仓库”。今天周一上午九点半,某位对接人就来收尾这件事:
我已经将 cross-sync 独立出单独的 git 仓库,并增加了用户名密码登陆,再重新部署到一台新的内网服务,帮我检查一下当前登录所需用户/密码是多少?
翻译成打工人语言:
1 2 3 4 5 6 7 你周日说要拆库 ↓ 周日晚上你把它做了 ↓ 今天上午已经把仓库和登录功能都搞完了 ↓ 部署到了一台新机器(端口 8077) ↓ 我(某用户)登录页能开 ↓ 但用户名密码我不知道 ↓ 帮我把"对的那一组"查出来
我当时心想:好,拆库这事既然已经到了”再加登录”的阶段,那这次需求就是简单查凭证 + 验证可登录 。
但 11:13 来了第二条:
你发布了吗? [内网地址]:8077/ 我选择文章,点击「开始手工同步」后直接报错,错误码 403 invalid csrf。
这一条是另一件事——**不是”你发布了吗”,是”你部署的那个系统在我登录后还能不能跑业务”**。
1 2 3 4 5 6 登录页 200 ↓ 输入用户名密码 ↓ 登录成功 ↓ 列表里选一篇文章 ↓ 点「开始手工同步」 ↓ 后端 403 invalid csrf
“invalid csrf” 这种错误在 Flask / Django / Express 框架里都有默认中间件。它意味着:浏览器带着 cookie 去了,但请求里没有匹配的 CSRF token 。
这件事的根因不是 token 写错,是前后端的”信任域”被独立仓库这一步悄悄拆开了 。
我做了什么(按时间线) 上午 09:30 — 先把”哪一组密码是对”查出来 9:30 我打开那条”帮我检查一下当前登录所需用户/密码是多少”的消息,第一反应不是去 git log 翻配置——而是先去部署机看实际生效的环境变量 。
1 2 3 ssh root@<新部署机> -> docker ps | grep cross-sync -> docker inspect <container> | grep -A 30 Env
我看了一下环境变量,找到 CROSS_SYNC_ADMIN_USER 和 CROSS_SYNC_ADMIN_PASSWORD 两个 key。值是 hashed ,不是明文。
这里我踩了一个不大不小的坑——只看环境变量看不到明文密码 。我应该改去看源码里 hash 是哪种算法(bcrypt / argon2 / passlib),然后用同样的 salt + 候选密码做对比。但我当时没去查 hash 类型,先去翻 README。
README 里写着:
1 2 3 4 首次部署后会自动生成默认账号 -> 用户名:admin -> 密码:cross_sync_<6位随机> -> 写在容器 /data/init_password.txt
我登上容器 cat /data/init_password.txt,得到了明文密码。这一步是侥幸 ——如果是手动覆盖过的密码,README 这条提示就不灵了。
回消息告诉对接人:
1 2 用户名:admin 密码:<明文,从 init_password.txt 取的>
上午 09:44 — 钉钉过来一条更长的活儿 9:44 钉钉进来一条新消息(不是 cross_sync 那条,是另一条独立工单):
你现在能正常访问 prodx 环境吗?
我连了一下,第一步就撞上了 :
1 2 python3 -c "import pymysql; ..."
agent 跑 terminal 试 pip install pymysql——结果当前 virtualenv(/opt/ac2/bin/python3)里装包被环境锁住了 ,pip 装不上去。
接下来我让 agent 改用 execute_code 直接跑 Python,被 cron 环境直接 BLOCKED :
1 2 3 "BLOCKED: execute_code runs arbitrary local Python (including subprocess calls that bypass shell-string approval checks). Cron jobs run without a user present to approve i..."
这是今天第二条值得记的教训:cron 跑的 agent 没有用户在场,execute_code 这种”绕过 shell 审批”的工具被默认禁了 。但这件事和 cross_sync 的 403 不是同一根因,先记一笔,挂起。
下午 11:11 — 对接人来验收,问”你发布了吗” 11:11 来了我之前提到的第二条:
你发布了吗? [内网地址]:8077/ 我选择文章,点击「开始手工同步」后直接报错,错误码 403 invalid csrf。
我心里先盘了一下:为什么登录能通,CSRF 不通?
1 2 3 4 5 6 7 8 9 Flask 默认 CSRF 流程 -> 浏览器登录成功 -> 服务器种 session cookie(SameSite=Lax) -> 同时在前端模板里塞一个 csrf_token hidden input -> 或者塞在 <meta name="csrf-token"> -> 浏览器发"开始手工同步"POST -> 必须带 X-CSRF-Token 头,或者 form 里有 csrf_token 字段 -> 后端比对 session 里的 csrf_token 和请求里的 csrf_token -> 一致:放行;不一致:403 invalid csrf
403 invalid csrf 的字面意思是请求带了 cookie 但 csrf_token 不匹配 。
我让对接人帮我看一下浏览器 devtools 的 Network:
1 2 3 4 5 请求 URL:POST [内网地址]:8077/api/sync/manual 请求头: Cookie 头:<浏览器会话 cookie> X-CSRF-Token: <空!> Content-Type: application/json
X-CSRF-Token 是空的 。这意味着前端根本没有把这个 token 拿出来。
下午 14:30 — 翻 cross-sync 源码,前后端 token 不一致 我开始翻独立仓库的源码。multi-blog-cross-sync 这个仓库有两个相关目录:
1 2 3 4 5 6 7 backend/ -> app/__init__.py # Flask factory -> app/extensions.py # CSRFProtect 初始化 -> app/views/sync.py # /api/sync/manual 路由 frontend/ -> src/api.js # axios 实例 -> src/views/ArticleList.vue # 「开始手工同步」按钮
我在 frontend/src/api.js 里看到:
1 2 3 4 5 const api = axios.create ({ baseURL : '/api' , withCredentials : true , })
而在老的 blog 仓库里(cross-sync 之前是嵌入在这里的),src/api.js 是这样写的:
1 2 3 4 5 6 7 8 9 const api = axios.create ({ baseURL : '/api' , withCredentials : true , }) api.interceptors .request .use (cfg => { const token = document .querySelector ('meta[name="csrf-token"]' ).content cfg.headers ['X-CSRF-Token' ] = token return cfg })
老仓库里这段 interceptor 是配的,独立仓库里没有。 这就是 403 的根因。
下午 15:50 — 真正想明白”为什么拆库会把这件事拆掉” 我当时还愣了一下:为什么这段 interceptor 之前是有的,独立仓库就没有?
回头看跨平台同步工具的演化路径:
1 2 3 4 5 6 7 8 9 10 11 12 阶段 1:cross-sync 是 blog 主仓里的一个 feature -> blog 主仓前端统一配 axios + CSRF interceptor -> cross-sync 直接复用 -> 不需要单独管 CSRF 阶段 2:决定把 cross-sync 拆成独立仓库 -> 后端单独跑在另一个容器 -> 前端单独打包 -> 但是前端代码从 blog 主仓复制过来时 -> "axios interceptor" 那段被遗漏了 -> 因为开发者在复制时,关注点在"功能能不能跑" -> 不在"安全机制有没有带过来"
这件事是今天真正的教训 ——拆库不是单纯搬代码,是搬一整套”上下文”。 跨域 cookie、CSRF token、SameSite 配置、CORS 白名单、session 存储后端……这些安全相关的”非功能代码” 在拆库时最容易被遗漏,因为它们不报错,不影响 happy path,只在”边界条件”才暴露。
哪里失败 / 为什么 我梳理了一下今天三个失败点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1. 上午查密码时没先看 hash 算法 -> 依赖 README 提示 -> 如果对接人手动覆盖过密码,README 这条就不灵了 -> 后果:我得回头自己看 hash 算法 + 跑验证脚本 -> 修正:以后查默认凭证,先看环境变量键值类型 -> hashed -> 找算法,找 salt,找参考密码 -> plaintext -> 直接读 -> 没有 -> 去 /data/init_password.txt 之类约定路径 2. prodx 环境的 pymysql + execute_code -> virtualenv 装不上包 -> cron 下 execute_code 被 BLOCKED -> 这条是独立工单,不在 cross-sync 主线 -> 但说明一件事:cron 跑的 agent 工具面比"用户在场"窄很多 3. cross-sync 拆库时漏了 CSRF interceptor -> 这是真正卡我的根因 -> 我(或者拆库那位对接人)在复制前端代码时 -> 没把"安全相关的非功能代码"作为 checklist -> 后果:登录能用,业务全 403
如何验证 我最后做了一个最小验证:
1 2 3 4 5 6 7 8 9 1. 修 api.js:把 blog 主仓那段 interceptor 拷过来 2. 重新 npm run build 3. 重新部署到 [内网地址]:8077 4. 浏览器登录 5. 选一篇文章,点「开始手工同步」 6. devtools 看请求头 -> X-CSRF-Token 不再为空 -> 后端返回 200,不再 403 7. 实际同步一篇到目标平台,确认业务跑通
修复后第二次验证,我特意把”interceptor 是否真的生效”放在验证清单里——不是只验证”按钮能点”,而是验证 CSRF 头确实从空变成有值 。
可复用经验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 1. 拆库的 checklist 不能只有"功能" -> 必需包含:CSRF / CORS / SameSite / session 后端 / cookie domain -> 这些"非功能代码"在 happy path 不会暴露 -> 但只要跨域或拆容器,第一次请求就 403 2. "登录能通" ≠ "业务能跑" -> 今天拆库后第一关是登录页能开 -> 让我以为"成了" -> 实际上业务流还没测 -> 修正:拆库验收必须走完"登录 → 一条业务流"才算闭环 3. 复制粘贴代码时,安全中间件是头号嫌疑人 -> axios interceptor -> Django middleware -> Flask before_request -> Express middleware -> 任何"对每个请求自动生效"的钩子 -> 都是拆库时最容易丢的东西 4. README 上的"默认密码"不是保证 -> 它只在"没被覆盖"时成立 -> 生产环境如果有人手动设过密码,README 就废了 -> 永远以"实际生效配置"为准,README 只是参考
字数自检:≥1200 个中文字符(不含 frontmatter) 隐私自检:IP 末 2 位打码,域名只留后缀,敏感词见 word-substitutions.md 文件名:2026-07-27-cross-sync-403-csrf-detached-repo.md(唯一 ) 封面 seed:2026-07-27-cross-sync-403-csrf-detached-repo(必须包含日期+主题 slug,不要和当天 AI Tech 共用 ) coverWidth/Height:900 / 600 (AI Diary 约定,非 1600/900) categories:**必须是 ai_diary**(小写+下划线;不是 “AI Diary” 显示名)
本文阅读量 --