一个字符就够了
误植域名攻击利用开发者想输入的名称与实际输入或粘贴的名称之间的差距。reqeusts 对应 requests,lodahs 对应 lodash,或用西里尔字母伪装拉丁字母,都会解析到攻击者控制的真实包。载荷常在安装时执行,任何测试都来不及发现。
工作机制
攻击者注册与流行包相近的名称,发布看似合理的版本。包可能带 postinstall 脚本,读取环境变量、SSH 密钥或 CI 令牌并外发;有些变体等到特定主机名或云元数据端点才行动,以躲避沙箱检测;还有些依赖真实包,让应用照常工作。
防御主要是流程性的:用 npm ci 从锁文件安装,尽可能禁用安装脚本,核验发布者身份和仓库链接,让新依赖经过评审。仓库会部署名称相似度检测,但它是建议性的,且总落后于新攻击。
何时使用与取舍
每次新增依赖都要检查,本地和 CI 都包括。带凭据运行的脚本风险最高,例如部署任务和登录了云会话的开发者笔记本。依赖镜像能降低暴露,但需要持续维护。
禁用安装脚本会破坏需要编译原生模块的包,因此通常按白名单放行。逐个审查依赖会拖慢交付。私有镜像提供控制点,却需要维护,并可能滞后安全更新。完美的名称核验并不存在,只能靠多层防御。
示例与常见误解
开发者照着教程执行 npm install crossenv,这是 cross-env 的拼写错误。恶意包的 postinstall 读取 npm_config_* 环境变量并发送到外部服务器;应用仍能构建,所以没人察觉。
常见误解是只有新手才会中招。从博客和 AI 生成的命令复制粘贴,会让拼写错误完全隐形,与经验无关。相关概念:依赖混淆、供应链安全、依赖固定。
代码示例
import unicodedata
SCRIPT_RANGES = {
"LATIN": [(0x0041, 0x024F), (0x1E00, 0x1EFF)],
"CYRILLIC": [(0x0400, 0x04FF)],
"GREEK": [(0x0370, 0x03FF)],
}
def script_of(char: str) -> str:
code = ord(char)
for name, ranges in SCRIPT_RANGES.items():
if any(start <= code <= end for start, end in ranges):
return name
return "OTHER"
def find_mixed_scripts(text: str) -> list[str]:
scripts = {script_of(char) for char in text if char.isalpha()}
return sorted(scripts - {"OTHER"})
def is_suspicious(text: str) -> bool:
scripts = find_mixed_scripts(text)
return "LATIN" in scripts and len(scripts) > 1
def explain(text: str) -> list[dict[str, str]]:
return [
{
"char": char,
"name": unicodedata.name(char, "UNKNOWN"),
"script": script_of(char),
}
for char in text
if char.isalpha() and script_of(char) not in {"LATIN", "OTHER"}
]
if __name__ == "__main__":
for sample in ["paypal", "pаypal", "admin"]:
print(sample, find_mixed_scripts(sample), is_suspicious(sample))
print(explain("pаypal"))
package main
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
)
func ConstantTimeEqual(a, b string) bool {
left := sha256.Sum256([]byte(a))
right := sha256.Sum256([]byte(b))
return subtle.ConstantTimeCompare(left[:], right[:]) == 1
}
func main() {
secret := "s3cr3t-token"
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-token"))
fmt.Println(ConstantTimeEqual(secret, "s3cr3t-tokeX"))
fmt.Println(ConstantTimeEqual(secret, "short"))
}