---
id: ERR-20260502-003
title: SSH 远程执行带 $ 的字符串（bcrypt hash / 密码）会被远端 shell 二次展开
date: 2026-05-02
tags: [ssh, shell, bcrypt, ops, pitfall]
---

## 现象

把 bcrypt hash `$2b$10$YCkv...URkiO` 通过 SSH 双引号传到远程 psql：

```bash
HASH='$2b$10$YCkvTSvnxIVQZUOw/s0a4u1MuW.LvsQcH5u17RqSQtn7G8Q/URkiO'
ssh user@host "docker exec ctn psql -c \"UPDATE users SET password_hash='$HASH' WHERE ...;\""
```

UPDATE 报告成功，但实际存进 DB 的是 `b0/s0a4u1MuW.LvsQcH5u17RqSQtn7G8Q/URkiO`——`$2b`、`$10`、`$YCkvTSvnxIVQZUOw/s0a4u` 全被吃掉。登录返回 401，毫无报错信息。

## 根因

层 1：本地 bash 把 `"$HASH"` 展开成完整字符串。
层 2：SSH 把这个字符串原样发到远端。
层 3：**远端 bash 又跑一次变量展开**——把 `$2`、`$10`、`$YCkv...` 当未定义变量替换成空串，只留下后半段 `b0/s0a4u...`（`$YCkvTSvnxIVQZUOw/s0a4u` 整段没了，留下后面的 `1MuW...`，但因为 `/` 不是变量名字符，扫描在 `s0a4u` 处停止——所以前缀就丢了 `0a4u` 之前的部分，但 `0a4u` 之后又拼回来了，得到 `b0/s0a4u1MuW...URkiO`）。
层 4：psql 收到的 SQL 把这个被腐蚀的 hash 写入。
层 5：bcrypt.compare 永远 false，登录 401，且 DB 看上去"成功"，没有任何报错。

## 解决方案（推荐）

**用 stdin 把 SQL 喂给远端 psql，避免远端 shell 解析 SQL 内容**：

```bash
HASH='$2b$10$...'
SQL="UPDATE platform_iam.users SET password_hash='$HASH', updated_at=NOW() WHERE username='itadmin';"
echo "$SQL" | ssh user@host 'docker exec -i ctn psql -U u -d d'
```

要点：
- SSH 命令用 **单引号**（`'docker exec -i ...'`），远端不解析。
- `docker exec -i`（带 `-i`）从 stdin 读 SQL。
- 本地 `$SQL` 在本地双引号内已展开为最终字符串，到远端就是字面量了。

## 替代方案

- 写临时 SQL 文件后 `scp` 上去再执行（更稳但多一步）。
- 用 `printf '%q' "$HASH"` 双重转义后再嵌入命令字符串（复杂、易错，不推荐）。

## 检测信号

凡是 SSH 远程 SQL UPDATE 后行为不符合预期，**第一件事是回查实际写入的值**——不要相信 `UPDATE 1` 的成功输出。psql 只保证语法对、行数对，不保证你"以为"传进去的内容真的进去了。

```sql
SELECT LEFT(password_hash, 20) FROM ... WHERE ...;
-- 如果开头不是 $2a / $2b / $2y，就是被吃了。
```

## 适用范围

任何远程 SSH 执行包含 `$`、反引号、`\` 的字符串场景：bcrypt hash、JWT secret、含特殊字符的密码、含 PostgreSQL `$$ ... $$` dollar-quoted 块的 SQL。
