公开 Cloud Run 服务如何从一个输入漏洞演变为云项目失陷

2026-07-15 40 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

公开的无服务器服务并不天然不安全,但它把自研代码、第三方依赖、运行时身份和云 API 放在了同一条攻击链上。一次目录遍历或命令注入,可能不只暴露容器内的文件,还会让攻击者借助服务账号权限访问 Secret Manager、Cloud Storage,甚至控制整个 Google Cloud 项目。

真正有效的防护不能只依赖修复某一行代码。团队需要同时约束应用输入、运行时身份、网络入口和横向访问路径。

攻击链为什么会越过容器边界

Cloud Run 和函数服务会自动管理底层基础设施,但部署进去的应用仍然要承担传统 Web 应用面对的风险,包括本地或远程文件包含、目录遍历、命令注入、易受攻击的第三方包以及业务逻辑缺陷。

以文件读取接口为例,如果程序直接执行 open(user_input),攻击者就可能读取:

  • 应用源码以及 requirements.txtpackage.jsongo.mod 等依赖清单;
  • 环境变量、配置文件和日志中的令牌或个人数据;
  • /proc/self/environ 等运行时信息;
  • 暴露内部服务地址、认证方式和后续注入点的业务代码。

命令注入的影响更大。若应用把用户输入交给 subprocess.run(..., shell=True),攻击者获得的通常是容器内远程代码执行能力。随后,他们可能尝试访问云平台元数据服务,取得当前服务账号的短期访问令牌。

短期令牌并不等于低风险。它在有效期内继承服务账号的 IAM 权限。如果 Cloud Run 使用默认 Compute Engine 服务账号,而该账号还保留宽泛的 Editor 权限,攻击者就可能读取密钥、修改资源、部署持久化服务或外泄存储数据。容器只是入口,IAM 权限才决定最终爆炸半径。

先在代码里切断文件读取与命令执行

WAF 可以拦截常见攻击特征,但不能替代输入校验。对于确实需要读取服务端文件的接口,可以这样实践:将访问范围固定在专用目录中,使用业务文件标识而不是任意路径,并在打开文件前验证解析后的真实路径。

下面的示例可直接作为 Functions Framework HTTP 函数运行。它只允许读取 data 目录中的两个公开文件,不接受调用者提供的文件路径。

from pathlib import Path

import functions_framework
from flask import jsonify

DATA_DIR = (Path(__file__).parent / "data").resolve()
ALLOWED_FILES = {
    "terms": "terms.txt",
    "status": "status.json",
}


@functions_framework.http
def read_public_file(request):
    payload = request.get_json(silent=True) or {}
    file_id = payload.get("file_id") or request.args.get("file_id")

    filename = ALLOWED_FILES.get(file_id)
    if filename is None:
        return jsonify(error="unknown file_id"), 400

    target = (DATA_DIR / filename).resolve()
    if DATA_DIR not in target.parents:
        return jsonify(error="invalid path"), 400

    try:
        return target.read_text(encoding="utf-8"), 200
    except FileNotFoundError:
        return jsonify(error="file not found"), 404

运行前创建如下文件:

.
├── main.py
├── requirements.txt
└── data
    ├── status.json
    └── terms.txt

requirements.txt 内容如下:

functions-framework==3.*

本地启动并测试:

python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
functions-framework --target=read_public_file --port=8080

curl -i -X POST http://127.0.0.1:8080 \
  -H 'Content-Type: application/json' \
  -d '{"file_id":"status"}'

如果业务需要调用系统工具,不要把完整命令交给用户,也不要启用 shell=True。应固定可执行文件和参数结构,通过映射表把外部输入转换为有限的内部值。例如图片转换服务可以只接受预定义尺寸,再以参数数组调用工具。若 Python 标准库或 SDK 能完成任务,则应直接使用库 API,进一步移除 shell 攻击面。

密钥同样不能写进源码、镜像层、.env 文件或普通环境变量配置中。应将其放入 Secret Manager,并只授权运行时服务账号读取实际需要的单个密钥。

用独立身份限制失陷后的权限

公开服务不应使用默认 Compute Engine 服务账号。可以这样实践:为每个服务或同类工作负载创建专用服务账号,并把权限限制到具体资源。

下面的命令假设服务只需读取一个 Cloud Storage 存储桶。执行前替换 PROJECT_IDREGIONBUCKET 和镜像地址:

export PROJECT_ID="my-project"
export REGION="europe-west3"
export BUCKET="my-public-data"
export SERVICE_ACCOUNT="public-reader@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam service-accounts create public-reader \
  --project="${PROJECT_ID}" \
  --display-name="Cloud Run public reader"

gcloud storage buckets add-iam-policy-binding "gs://${BUCKET}" \
  --member="serviceAccount:${SERVICE_ACCOUNT}" \
  --role="roles/storage.objectViewer"

gcloud run deploy public-reader \
  --project="${PROJECT_ID}" \
  --region="${REGION}" \
  --image="${REGION}-docker.pkg.dev/${PROJECT_ID}/apps/public-reader:latest" \
  --service-account="${SERVICE_ACCOUNT}" \
  --ingress="internal-and-cloud-load-balancing" \
  --no-allow-unauthenticated

这里有两个关键约束:

  • roles/storage.objectViewer 绑定在指定存储桶,而不是整个项目;
  • Cloud Run 只接受内部流量和来自 Cloud Load Balancing 的流量,公网入口交给外部七层负载均衡器管理。

如果服务需要读取密钥,应把 roles/secretmanager.secretAccessor 授予具体 Secret,而不是项目级授权。还要检查项目中默认服务账号是否残留 Editor、Owner 或其他历史宽权限。

把公网暴露集中到负载均衡与 WAF

必须公开访问的 Cloud Run 服务,可以放在外部 Layer 7 Application Load Balancer 后面,并限制服务自身的 ingress。这样可以集中管理 TLS、请求头、日志、速率限制和身份策略,同时接入 Cloud Armor。

Cloud Armor 的预配置规则可用于拦截常见 LFI 和 RCE 特征。例如在安全策略中分别加入以下表达式:

evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 3})
evaluatePreconfiguredWaf('rce-v33-stable', {'sensitivity': 3})

可以这样通过 CLI 创建规则。执行前确认 public-serverless-policy 已经绑定到负载均衡器使用的后端服务:

gcloud compute security-policies rules create 1000 \
  --security-policy="public-serverless-policy" \
  --expression="evaluatePreconfiguredWaf('lfi-v33-stable', {'sensitivity': 3})" \
  --action=deny-403 \
  --description="Block common local file inclusion attempts"

gcloud compute security-policies rules create 1010 \
  --security-policy="public-serverless-policy" \
  --expression="evaluatePreconfiguredWaf('rce-v33-stable', {'sensitivity': 3})" \
  --action=deny-403 \
  --description="Block common remote code execution attempts"

灵敏度提高后可能出现误报,应先在预发布环境验证正常请求,观察 Cloud Armor 日志,再逐步启用阻断。WAF 规则匹配的是已知模式,经过编码、拆分或业务协议封装的恶意输入仍可能绕过,因此不能把 WAF 当作代码修复方案。

面向内部员工或合作方的服务还可以在负载均衡层接入 Identity-Aware Proxy。真正无需身份的公共 API,则应配置配额、请求大小限制、超时和速率限制,避免漏洞之外的资源消耗攻击。

从单个服务扩展到云架构

当 Cloud Run 通过 Direct VPC egress 或 VPC Access Connector 访问内部资源时,远程代码执行也可能变成横向移动入口。公共工作负载应尽量部署到隔离的服务项目中,并使用 VPC Service Controls、细粒度访问策略和出口控制限制数据外泄。

上线前可以按以下清单审查:

  • 公共 Cloud Run 是否与关键生产资源处于隔离项目;
  • 是否使用专用服务账号,并移除默认账号的宽权限;
  • IAM 是否绑定到具体 Bucket、Secret 或其他资源;
  • Cloud Run ingress 是否限制为负载均衡入口;
  • 是否启用 Cloud Armor、速率限制、集中日志与告警;
  • CI/CD 是否执行依赖扫描、静态分析、密钥扫描和持续安全测试;
  • AI 生成代码是否经过人工审查,并在隔离环境中测试;
  • VPC 出口和服务边界是否能阻断横向移动与数据外泄;
  • 是否通过 Security Command Center 等能力监测凭据访问、侦察、脚本执行和反向 shell 行为。

公开无服务器服务的安全目标不是保证应用永远没有漏洞,而是让单个漏洞难以继续升级。输入校验负责阻止初始利用,最小权限 IAM 限制攻击者能做什么,负载均衡与 WAF 提供外围过滤和可见性,项目隔离与服务边界则负责控制最终影响范围。


相关推荐