适用场景
把 API 后端、定时任务、文件处理交给云函数后,运维工作量下降,但攻击面并没有消失,只是换了位置。云函数安全加固要解决的是 Serverless 特有的四类问题:事件参数注入、环境变量中的明文密钥、IAM 角色过度授权、以及 HTTP 触发器裸奔。本文以函数计算类平台(阿里云函数计算 FC、腾讯云 SCF、AWS Lambda 均通用)为例,给出最小权限与运行时加固的可复制配置。
前置条件
- 已部署至少一个云函数,具备控制台与 CLI 访问权限(
aliyun/tccli/aws) - 函数使用 Node.js 18+、Python 3.10+ 或 Java 11+ 运行时
- 已开通密钥管理服务(KMS)与日志服务
- 有测试账号可用于越权与拒权验证
原理说明
Serverless 的信任边界与传统服务器不同,主要风险来自三类机制:
- 事件注入:函数的入参
event是攻击者完全可控的 JSON。若函数把event中的字段直接拼进 SQL、Shell 命令、模板或下游 API 调用,就等价于把注入点直接暴露在公网。常见形态是 API 网关透传的pathParameters、queryStringParameters未做白名单校验。 - 身份即权限:每个函数绑定一个执行角色,该角色通常被授予了平台默认策略(如
AliyunFCDefaultRolePolicy或含Resource: "*"的自定义策略)。一旦函数被注入利用,攻击者拿到的是整个云账号的临时凭证,可以直接操作 OSS、RDS、甚至修改角色本身。 - 密钥与依赖:环境变量在控制台与日志中默认可读,硬编码的 AccessKey 极易泄露;Layer 与公共依赖包则是供应链投毒的常见载体。
此外,HTTP 触发器若配置为匿名访问,等于把函数直接挂到公网,缺少 WAF 与签名校验时会成为扫描器的靶子;并发与超时未限制时,一次洪泛即可把费用打到四位数。
操作步骤
1. 收敛执行角色到最小权限
不要使用平台默认策略,改为显式枚举 Action 与 Resource:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": ["oss:GetObject", "oss:PutObject"],
"Resource": ["acs:oss:*:*:my-app-bucket/uploads/*"]
},
{
"Effect": "Allow",
"Action": ["log:PutLogs"],
"Resource": ["acs:log:*:*:project/my-app-logs/*"]
}
]
}
落地时用 CLI 绑定到函数:
aliyun fc UpdateFunction \
--serviceName my-service \
--functionName api-handler \
--runtime python3.10 \
--handler index.handler \
--role acs:ram::1234567890:role/my-app-fc-role
加固前务必先用 aliyun ram ListPoliciesForRole --RoleName my-app-fc-role 核对是否残留 *:* 类策略并解绑。
2. 密钥全部走 KMS,禁止写入环境变量明文
# 写入加密配置
aliyun kms CreateSecret --SecretName fc-db-password \
--SecretData "$(openssl rand -base64 24)" --VersionId v1
# 只把 SecretName 放进环境变量,不在变量里放真实口令
aliyun fc UpdateFunction --serviceName my-service --functionName api-handler \
--environmentVariables "{\"DB_SECRET_NAME\":\"fc-db-password\",\"REGION\":\"cn-hangzhou\"}"
函数运行时按需要临时拉取:
# Python 侧:从 KMS 拉取一次并缓存在进程内
from aliyunsdkcore.client import AcsClient
from aliyunsdkkms.request.v20160120 import GetSecretValueRequest
import os, json, functools
@functools.lru_cache(maxsize=1)
def get_secret():
c = AcsClient(os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"],
os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"], "cn-hangzhou")
r = GetSecretValueRequest.GetSecretValueRequest()
r.set_SecretName(os.environ["DB_SECRET_NAME"])
return json.loads(c.do_action_with_exception(r))["SecretData"]
更好的做法是给函数角色只授予 KMS 的 Decrypt 权限,彻底取消函数的长期 AccessKey。
3. 事件参数白名单校验
import re, json
ALLOW_KEYS = {"id", "page", "size", "type"}
ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
def handler(event, context):
try:
ev = json.loads(event) if isinstance(event, str) else event
except Exception:
return {"statusCode": 400, "body": "bad json"}
# 1) 键白名单,拒绝任何未声明字段
if set(ev.keys()) - ALLOW_KEYS:
return {"statusCode": 400, "body": "unknown field"}
# 2) 值格式校验,杜绝拼接注入
if not ID_RE.match(str(ev.get("id", ""))):
return {"statusCode": 400, "body": "bad id"}
# 3) 数值范围限制,避免超大分页拖垮下游
page = max(1, min(int(ev.get("page", 1)), 1000))
size = max(1, min(int(ev.get("size", 20)), 100))
return {"statusCode": 200, "body": json.dumps({"page": page, "size": size})}
禁止把 event 直接传给 os.system、subprocess(shell=True)、字符串拼接 SQL,或作为对象存储路径前缀使用。
4. HTTP 触发器加签名与 WAF
# 关闭匿名访问,改为需要签名校验
aliyun fc UpdateTrigger --serviceName my-service --functionName api-handler \
--triggerName http-trigger --triggerType http \
--triggerConfig "{\"authType\":\"function\",\"methods\":[\"POST\"]}"
平台侧建议同步开启三道控制:把 API 网关或自定义域名接入 WAF 并启用 CC 防护;限制请求体大小(如 1MB)与查询串长度;对写操作接口强制 HMAC 签名 + 时间戳 + nonce,时间戳窗口不超过 300 秒,nonce 在缓存中做一次性校验。
5. 出网收敛与运行时限制
# 把函数放进 VPC,仅通过 NAT 网关出网,且 NAT 侧只放行必要目标
aliyun fc UpdateFunction --serviceName my-service --functionName api-handler \
--vpcConfig "{\"vpcId\":\"vpc-bp1xxxx\",\"vSwitchIds\":[\"vsw-bp1xxxx\"],\"securityGroupId\":\"sg-bp1xxxx\"}"
# 限制单实例并发、超时与实例数,防止被当成免费算力
aliyun fc PutProvisionConfig --serviceName my-service --functionName api-handler \
--qualifier LATEST --target 0
aliyun fc UpdateFunction --serviceName my-service --functionName api-handler \
--timeout 10 --memorySize 256 --instanceConcurrency 10
同时在函数配置中设置 最大实例数上限(如 20),并给账号配置费用告警,避免并发洪泛直接转化为账单事故。
6. 依赖锁定与日志脱敏
# Node.js:使用锁文件 + 完整性校验
npm ci --omit=dev --ignore-scripts
npm audit --audit-level=high
# Python:固定版本哈希
pip install --require-hashes -r requirements.lock
# 日志脱敏:禁止打印 event 全文与凭证字段
SENSITIVE = {"password", "token", "secret", "authorization", "cookie"}
def safe_log(obj):
if isinstance(obj, dict):
return {k: ("***" if k.lower() in SENSITIVE else safe_log(v)) for k, v in obj.items()}
if isinstance(obj, list):
return [safe_log(i) for i in obj]
return obj
--ignore-scripts 可阻断依赖安装期脚本执行,是防 Layer 与 npm 投毒的有效手段。
配置验证
- 越权测试:用函数角色尝试访问未授权的存储桶,应返回 AccessDenied:
aliyun oss ls oss://other-bucket --access-key-id $AK --access-key-secret $SK。 - 注入测试:向 HTTP 触发器发送含额外字段的 JSON,例如
{"id":"1","cmd":"whoami"},应返回unknown field。 - 密钥泄露检查:执行
aliyun fc GetFunction --serviceName my-service --functionName api-handler,确认 output 的environmentVariables中无明文口令。 - 日志脱敏检查:在日志服务中检索
password、token关键字,命中记录应只显示***。 - 并发上限检查:用
ab -n 500 -c 100打一轮压测,控制台应显示实例数被限制在配置上限,且无异常费用增长。
常见问题
Q1:函数只需要读一个存储桶,为什么还要自定义策略,直接用默认角色不行吗?
默认角色通常包含对象存储全量读写、日志写入乃至其它平台服务的访问权限。函数一旦被注入利用,攻击者可以用函数身份遍历、下载甚至删除账号下所有存储桶的数据,还能借助日志服务的写入权限伪造审计记录。最小权限的意义在于把「一台函数失陷」的爆炸半径限制在单桶前缀之内,这与传统服务器上的权限收敛原则完全一致。
Q2:密钥放在 KMS 里,是不是就绝对安全了?
不是。KMS 解决的是「密钥不以明文形式存储与分发」,但函数运行时有权限读取密钥,所以仍要控制三点:一是函数角色只授予特定 Secret 的读权限,避免横向遍历;二是密钥缓存在进程内并使用 lru_cache,减少调用次数与日志暴露面;三是密钥定期轮换,并开启 KMS 的访问审计,一旦出现异常读取立即告警。真正的兜底是密钥可随时轮换且轮换成本极低,而不是假设它永远不会泄露。
Q3:函数并发限制设小了,会不会影响业务体验?
应先测量正常峰值的并发数(控制台可查看实例与并发指标),把上限设为峰值的一点五到两倍,再配合 API 网关侧的限流与排队。这样既能挡住突发洪泛,又不会在日常流量下产生 429。压测时观察冷启动比例,如果因为实例上限导致大量请求排队,应优先优化单次执行耗时,而不是无脑抬高上限。
总结
云函数安全加固的核心是把 Serverless 当成「随时可能失陷的边缘节点」来设计:权限最小化(自定义策略、单桶前缀、只读 KMS)、输入强校验(键白名单 + 值格式 + 范围限制)、入口加固(关闭匿名触发、强制签名、WAF 前置)、运行时收敛(VPC 出网、并发与超时上限、费用告警)、供应链与日志(锁文件 + 禁用安装脚本 + 日志脱敏)。五层叠加后,即使某个函数被成功利用,攻击者拿到的也只是一个权限受限、网络受限、日志留痕的临时身份。