Margrop
Articles366
Tags630
Categories7

Categories

1password 4-bit 量化 6-DoF SLAM AC ACL 切换 ACP AI AI Agent AI Coding Assistant AI Tech AI 安全 AI 日记 AI编程助手 AI辅助 AI辅助编程 AP API ASR Agent Agent 检索 Agent 系统 AgentPlan Alertmanager Android 17 Antigravity AppDaemon AppWorld Aqara Attention Blog CC-Switch CI/CD CLI Tools CLI 工具 CLI工具 CSRF Cache Hit Rate Caddy Categories 404 Claude Code Claude Sonnet ClawLoader Cloudflare Codex Coding Plan Coding Plan Dashboard Computer Use Context Compression Cookie 认证 Cosmos-H-Dreams Cost Optimization Cron D1 DFIR Date Diagrams.net Diary Diffusers Diffusion Docker Docker Compose Efficiency Tools Electerm Embedding English FSDP2 Fable 5 Fireworks AI FlashAttention FlashDreams Flask GLM 5.2 GPT-4.1 GPT-5.6 GPT-Red GPU 加速 GPU 性能分析 Gateway Gemini Gemini 3.5 Flash Gemini API Gemini CLI Gemini Omni Flash Gemma 4 12B GitHub GitHub Actions Google AI Grabette Gripette HA HADashboard Hermes HermesAgent Hexo HomeAssistant Hugging Face Hugo IBM Research IP IPv4 Isaac Lab Java Kimi Code Kimi K3 LLM Router LVM‑Thin LeRobot Linux Live Translate LoRA MCP MacOS Managed Agents Markdown Memory 上限 Microsoft 365 Copilot MiniMax Model Routing MuJoCo Warp Multi-Agent MySQL NAS NVIDIA NeMo Automodel Nemotron 3 Embed NewAPI Newton Nginx Node-RED Node.js Nunchaku OOM On-device AI Open Source Open Viking OpenAI OpenClaw OpenCode OpenResty OpenWrt PPPoE Physical AI Pollen Robotics Portainer PostgreSQL ProcessOn Prometheus Prompt Injection Proxmox VE PyTorch RPC RTEB Real-Time Inference Red Teaming SOCKS5 SOCKS5 代理 SSL SVDQuant Self-Forcing Distillation Session Shell Skill 管理 Subagent Surgical Robotics TTS TimeMachine UML Uptime Kuma VPN VPS VoiceEQ Web WebSocket Windows Workers World Foundation Model activate ad adb adblock agent aligenie aliyun alpine annotation aop authy autofs backup baidupan bash bitwarden boot brew browser caddy2 cdn centos cert certbot charles chat chrome classloader client clone closures cloudflare cmd command commit container crontab cross_sync ctyun ddsm demo dependency deploy developer devtools dll dns docker domain download draw drawio dsm dump dylib easytier edge exception export fail2ban feign firewall-cmd flow frp frpc frps fuckgfw function gcc gfw git github golang gperftools gridea grub gvt-g hacs havcs heap hello hexo hibernate hidpi hoisting homeassistant hosts html htmlparser https iKuai idea image img img2kvm immortalwrt import index install intel io ios ip iptables iptv ipv6 iso java javascript jetbrains jni jnilib jpa js json jsonb jupter jupyterlab jvm k8s kernel key kid kms kodi koolproxy koolproxyr kvm lan lastpass launchctl learning lede letsencrypt linux live low-code lvm lxc m3u8 mac macos mariadb markdown maven md5 mdadm microcode mirror modem modules monitor mount mstsc mysql n2n n5105 nas network nfs node node-red nodejs nohup notepad++ npm nssm ntp oop openfeign openssl os otp ovz p14 packet capture pat pdf pem perf ping pip plugin png powerbutton print pro proxy pve pvekclean python qcow2 qemu qemu-guest-agent rar reboot reflog remote remote desktop renew repo resize retina root route router rule rules runtime safari sata scipy-notebook scoping scp self-play server slmgr so socks source spk spring springboot springfox ssh ssl stash string supernode support svg svn swagger sync synology systemctl systemd tap tap-windows tapwindows telecom template terminal tls tmux token totp tvbox txt ubuntu udisk ui undertow uninstall unlocker upgrade url v2ray vLLM vhd vim vlmcsd vm vmdk web websocket wechat windows with worker wow xiaoya xml yum zip 个人品牌 中国电信 云电脑 交换机 人机协作 代理 企业 AI 优化 体检 值班 健康检查 光猫 公众号 公网IP 内存 内存优化 内容发布 内网 内网IP 内网渗透 写作 分布式训练 升级 协作 博客 博客同步 博客改名 博客运维 反向代理 变更审计 后台 Review 启动 告警 告警优化 周一 周一傍晚 周一焦虑 周五 周六 周四傍晚 周报 周日 周末 夏令时 多智能体 多节点 多节点管理 天猫精灵 天翼云 字体大小 安全 安全事件 安装 定时任务 容器 容器网络 导入 小米 工作感悟 工具调用 工程实践 常用软件 广告屏蔽 序列号 应用市场 开放 API 开源模型 开源项目 异常 异步任务 微信公众号 心智成长 心跳 心跳检查 性能优化 感悟 打工 打工人 打工人日记 扩散模型 技术 抓包 排查 推理加速 描述文件 故障 故障排查 效率 效率工具 数据 旁路由 无服务器 日记 时区 显卡虚拟化 智能体集成 智能家居 智能音箱 服务器 服务管理 本地安装 机器人仿真 机器人数据采集 权限管理 架构 梯子 模块 模型推理 流程 流程图 浏览器 漫游 激活 火山引擎 火绒 灾难恢复 焦虑 独立仓库 玄学 生活 电信 画图 监控 监控系统 直播源 直觉 磁盘 磁盘故障 端口 端口冲突 端口扫描 管理 续期 网关 网络 网络风暴 群晖 群晖 NAS 脚本 脚本优化 腾讯 自动化 自动化反思 自动化运维 自动恢复 自动攻击 虚拟机 认证 证书 评测基准 语雀 语音 AI 质量检查 超时 跨平台 路由 路由器 软件管家 软路由 运维 运维日常 运维监控 连接保活 连接问题 通信机制 通知 部署 配置 钉钉 镜像 镜像源 门窗传感器 问题排查 防火墙 阿里云 阿里源 集客 需求变更 飞书

Hitokoto

Archive

周一把 cross-sync 拆成独立仓库再部署,下午点"开始手工同步"直接 403 invalid csrf:今天学到的不是密码错,是"前后端共享一套 cookie / CSRF 配置"这件事被独立仓库拆掉了

周一把 cross-sync 拆成独立仓库再部署,下午点"开始手工同步"直接 403 invalid csrf:今天学到的不是密码错,是"前后端共享一套 cookie / CSRF 配置"这件事被独立仓库拆掉了

笔名:小六 / 上海 / 1995 女 / 某互联网公司打工人

周一整天都在 cross_sync 拆库后修登录 + 403

一句话结论

今天把 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_USERCROSS_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; ..."
# ModuleNotFoundError: No module named '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,
})
// 没有 CSRF interceptor

而在老的 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” 显示名)

本文阅读量 --
Author:Margrop
Link:https://blog.margrop.com/post/2026-07-27-cross-sync-403-csrf-detached-repo/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可