前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8481
  • 生产级AI语音SDK集成实战:Python Browser Mobile 避坑指南
  • AI Agent实验:会撒谎、会投票杀同类、会研究如何存活
  • 推理模型隐藏思考过程的四种方式及计费陷阱
  • AI Agent 将漏洞利用开发压缩至分钟级
  • Pi Agent:统一多模型 LLM API 的 AI 编程 Agent 工具链
  • LibreChat v0.8.8 RC:ChatGPT 克隆支持 Agent 管理 API
  • LandingAI 文档智能抽取 Gen2:原子级引用+按字符计费
  • 不用向量数据库做个人 RAG:grep 级检索也够用
  • Claude Code vs Cursor:按任务场景选择 AI 编程工具的完整指南
  • 2026 年五大主流模型生产选型指南
  • Agent 记忆层测试:六条真正能发现腐化的断言
  • Agent 生成的限流器如何绕过多租户隔离测试
  • 周末 Agent 项目攻略:用permit文件管控写操作
  • squash-merge 后 HEAD~1 指向无关代码的 Agent bug 分析
  • AI 生成的 Schema 变更:别让自由派 Diff 绕过锁
  • AI 写的代码编译过了,但环境变量、锁文件、日志全踩坑
  • Skild AI S1:仅凭一段视频,机器人学会翻煎饼
  • Java 27 发布:后量子 TLS 与对象头压缩等 9 项 JEP
  • DeepSeek 算子负责人自述:AI 一年内从查文档到独立优化 CUDA 算子
  • 编程任务 LLM 大模型盲测:4 款模型代码质量实测
  • Agent 循环常见路径陷阱自查清单
  • 你的 CDN 可能正在偷偷屏蔽所有 AI 爬虫
  • 我用Electron给Meta Muse Code CLI做了个桌面客户端,核心教训是PTY交互设计
  • Simon Willison 实战:通过 WebSocket 在浏览器里调 Gemini Live
  • 谷歌TPU集群迈入百万芯时代,电力成AI扩张核心瓶颈
  • AI 对话机器人的隐藏指令系统详解
  • 2026 年生产级 AI Agent 的上下文工程指南
  • OpenAI Agent 被指批量投毒 RubyGems 事件分析
  • Google 发布 Gemini 3.8 Live:支持 97 语言实时切换的语音 Agent 模型
  • AI 爬虫实际读取的是原始 HTML 还是渲染后 DOM
  • Next.js/Node单仓库中多租户邮件定时任务的安全隔离实践
  • AI Agent 真实生产故障:可观测性与恢复模式
  • 美国政府令 Anthropic Fable 5 下线事件
  • Google发布Gemini 3.8 Live语音模型,价格仅为GPT-Live-1的零头
  • Gemini企业平台零信任AI Agent运行时防护
  • AI Agent 正在冲垮生产环境:构建 SDLC 防护栏的工程实践
  • Gemini 3.8 Live 发布:扩展思考能力版本登场
  • Azure SRE Agent:Agent 自动化运维,人类做决策
  • AWS Bedrock 提示缓存实战:输入 Token 成本最高降 90%
  • 基于SageMaker Serverless构建AI商品打标系统:Qwen3微调实战
  • 零成本打造 AI 友好的个人作品集:llms.txt + JSON-LD 实战
  • AI时代招聘失效:面试该考什么
  • 已加载 42 / 8481
8.0
热点
AI SCORE
编程提效2026-09-16 12:32

Agent 生成的限流器如何绕过多租户隔离测试

dev.to · AI#AI编程#多租户#安全
Editor brief · 编辑速览

文章揭示面试中 Agent 生成的 HTTP 429 限流代码看似通过测试但实际存在跨租户配额泄漏,并提供了针对多租户隔离的隐藏测试设计方法。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

为什么这道题仍然能筛选候选人

针对 HTTP 429 处理的开公 coding 题目,如今已被面试中使用的 coding 模型广泛背熟。候选者或 Agent 可以抛出一个 IP 地址到 reset 时间戳的 Map,却并不理解隔离本身。评审因此需要一道公开展示用例看起来普普通通、而隐藏用例会惩罚全局 key 的题目。Fail-open 存储和挂钟耦合同样属于那一批隐藏用例,因为它们能逃过 fixture 驱动的生成。

本文档面向已允许在 take-home 环节使用 assistant 的 JavaScript 服务端面试。说明不禁止 Agent,也不按候选者使用的编辑器品牌打分——只打分最终模块在 assistant 会话结束后是否仍然恪守租户边界。

面试官应将以下代码块复制进 take-home 题目中,不要额外添加产品叙事。下面的公开 fixture 是候选者唯一能看到的测试文件。隐藏的隔离用例留在面试官私人仓库直到评分当天。

构建一个生产形态的 HTTP 限流器,面向多租户公有 API,不连接真实网络。每个请求携带 tenantId、route 和一个可选的 subject(如来自调用方的用户标识符)。限流器必须在路由 handler 运行之前、在任何下游副作用之前做出允许或拒绝决定。拒绝的调用返回 429 状态码和 Retry-After 头,其值取窗口结束前的整秒数。

限额按租户和按路由计算,绝不跨进程全局计算,也绝不单独从对端 IP 推导。一个噪声租户不得饿死共享同一 Node.js 实例或 worker 的另一个租户。当后端存储抛出异常时,限流器必须 fail closed 并以 429 拒绝请求。时钟必须可注入,以便测试可以推进时间而无需 sleep 或 stub Date.now()。

交付 createLimiter(store, clock, policies) 以及一个纯函数 check(request),后者返回决策对象。包含针对隔离、窗口过期、fail-closed 行为和注入时钟上 Retry-After 算术的测试。

语言为 Node.js,不要求框架,评分模块中不含 HTTP listener。

Store 是一个注入接口,仅暴露 get、set 和 incr。

Clock 为 { nowMs() },且必须是窗口算术中唯一的时间来源。

Policies 以 { route, windowMs, max } 条目传入,以精确路由字符串为主键。

配额状态不得存在于模块级 Map、Date.now() 或 setTimeout 中。

日志不得打印 Authorization 头、Cookie 或原始 bearer token。

候选者可见的公开 fixture

// public-fixture.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';

export function memoryStore() {
  const m = new Map();
  return {
    async get(k) {
      return m.get(k) ?? null;
    },
    async set(k, v) {
      m.set(k, v);
    },
    async incr(k, n = 1) {
      const cur = (m.get(k) ?? 0) + n;
      m.set(k, cur);
      return cur;
    },
  };
}

export function fakeClock(start = 1_000_000) {
  let t = start;
  return {
    nowMs: () => t,
    advance: (ms) => {
      t += ms;
    },
  };
}

test('allows traffic under the published limit', async () => {
  const clock = fakeClock();
  const limiter = createLimiter(memoryStore(), clock, [
    { route: 'GET /v1/items', windowMs: 60_000, max: 2 },
  ]);
  const req = { tenantId: 't1', route: 'GET /v1/items', subject: 'u1' };
  assert.equal((await limiter.check(req)).allowed, true);
  assert.equal((await limiter.check(req)).allowed, true);
});

公开 fixture 故意做得很薄,这样一个背熟的 429 代码片段在首次运行时仍然看起来是完整的。在让这个文件变绿之后就停手的 Agent,通常会在隐藏用例的租户 key 或存储错误上折戟。

评审应按提交的模块和隐藏测试打分,而非 README 语气或额外的后端。代码风格小瑕疵、TypeScript 转换和未使用的 Redis 客户端都救不回一个缺失的租户 key。需要在隔离和 fail-closed 拿到满分之后,才讨论其他评分项。

参考实现

以下示例是供面试官教学的草稿,而非库发布。保持隐藏测试不在候选者克隆的仓库中。调用方应将 allowed、status 和 retryAfterSec 视为 check 的唯一契约。

// limiter.js
export function createLimiter(store, clock, policies) {
  const policyByRoute = new Map(policies.map((p) => [p.route, p]));

  function keyFor(req, windowStart) {
    return `rl:${req.tenantId}:${req.route}:${windowStart}`;
  }

  function windowStart(now, windowMs) {
    return now - (now % windowMs);
  }

  function retryAfterSec(now, start, windowMs) {
    return Math.max(1, Math.ceil((start + windowMs - now) / 1000));
  }

  async function check(req) {
    if (!req || typeof req.tenantId !== 'string' || typeof req.route !== 'string') {
      return { allowed: false, status: 429, retryAfterSec: 1 };
    }

    const policy = policyByRoute.get(req.route);
    if (!policy) {
      return { allowed: true, status: 200, retryAfterSec: 0 };
    }

    const now = clock.nowMs();
    const start = windowStart(now, policy.windowMs);
    const key = keyFor(req, start);

    let count;
    try {
      count = await store.incr(key, 1);
      if (count === 1) {
        await store.set(`${key}:exp`, start + policy.windowMs);
      }
    } catch {
      return {
        allowed: false,
        status: 429,
        retryAfterSec: retryAfterSec(now, start, policy.windowMs),
      };
    }

    if (count <= policy.max) {
      return { allowed: true, status: 200, retryAfterSec: 0 };
    }

    return {
      allowed: false,
      status: 429,
      retryAfterSec: retryAfterSec(now, start, policy.windowMs),
    };
  }

  return { check };
}

将这个模块包装成 Express 或 Fastify 中间件不在评分范围内。如果面试官需要 HTTP 接线,可以在隐藏用例全绿之后再加。

面试官应保密的隐藏测试

这些用例才是真正的过滤器。不要把它们粘贴到候选者 prompt 或候选者能读取的 assistant 对话中。

// hidden.isolation.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';
import { memoryStore, fakeClock } from './public-fixture.test.js';

test('does not spend tenant B quota on tenant A traffic', async () => {
  const clock = fakeClock();
  const limiter = createLimiter(memoryStore(), clock, [
    { route: 'GET /v1/items', windowMs: 60_000, max: 1 },
  ]);
  const a = { tenantId: 't-a', route: 'GET /v1/items' };
  const b = { tenantId: 't-b', route: 'GET /v1/items' };
  assert.equal((await limiter.check(a)).allowed, true);
  assert.equal((await limiter.check(a)).allowed, false);
  assert.equal((await limiter.check(b)).allowed, true);
});

test('does not share budget across different routes', async () => {
  const clock = fakeClock();
  const limiter = createLimiter(memoryStore(), clock, [
    { route: 'GET /v1/items', windowMs: 60_000, max: 1 },
    { route: 'POST /v1/items', windowMs: 60_000, max: 1 },
  ]);
  const getReq = { tenantId: 't1', route: 'GET /v1/items' };
  const postReq = { tenantId: 't1', route: 'POST /v1/items' };
  assert.equal((await limiter.check(getReq)).allowed, true);
  assert.equal((await limiter.check(getReq)).allowed, false);
  assert.equal((await limiter.check(postReq)).allowed, true);
});

test('fails closed when incr throws', async () => {
  const clock = fakeClock();
  const store = {
    async get() {
      return null;
    },
    async set() {},
    async incr() {
      throw new Error('redis down');
    },
  };
  const limiter = createLimiter(store, clock, [
    { route: 'GET /v1/items', windowMs: 60_000, max: 10 },
  ]);
  const res = await limiter.check({ tenantId: 't1', route: 'GET /v1/items' });
  assert.equal(res.allowed, false);
  assert.equal(res.status, 429);
  assert.ok(res.retryAfterSec >= 1);
});

test('opens a new window after injected time passes', async () => {
  const clock = fakeClock();
  const limiter = createLimiter(memoryStore(), clock, [
    { route: 'GET /v1/items', windowMs: 1_000, max: 1 },
  ]);
  const req = { tenantId: 't1', route: 'GET /v1/items' };
  assert.equal((await limiter.check(req)).allowed, true);
  assert.equal((await limiter.check(req)).allowed, false);
  clock.advance(1_000);
  assert.equal((await limiter.check(req)).allowed, true);
});

先运行公开文件,再运行隐藏文件,使用同一工作树和同一个 limiter.js。

node --test public-fixture.test.js
node --test hidden.isolation.test.js

评审应关注 Agent 输出中的以下模式,因为它们会让公开 fixture 变绿、却在租户上崩溃。

以 IP 为 key 的全局 Map。模块编译通过且公开测试通过,然而租户 B 在租户 A 耗尽唯一计数器后也被阻止。

存储错误时 fail open。生成的注释说可用性比配额更重要,这会将一次缓存故障变成一笔无上限的账单。

Date.now() 混入窗口算术。推进假时钟的隐藏测试永远不会使窗口过期,所以过期用例会 false-fail 或挂起。

Key 中遗漏了 route。GET /v1/items 和 POST /v1/items 共享一个配额,这在大多数公有 API 中是一个产品缺陷。

Retry-After 设为整个窗口。客户端在一次本应两秒后在注入时钟上就清空的拒绝后要等待一分钟。

Subject 被用作唯一 key。缺失 subject 会导致限制失效,而盗取的 subject 字符串会错误地节流调用方。

跨 worker 的模块内存状态。两个进程各自允许最大请求数,静默地将公布的配额翻倍。

以上失败无需恶意候选者或损坏的编译器。它们出现在 assistant 针对可见 fixture 优化然后停手之时。

使用 Agent 而不洗白成绩

已经允许候选者使用 assistant 的团队,仍应在 prompt 发布前冻结隐藏测试。Assistant 可能从公开文件 draft 出 limiter.js。评审仍然运行从未粘贴进对话的隔离和 fail-closed 用例。

声明:本文作为 MonkeyCode 产品推广的一部分撰写。需要在 assistant 迭代中持有独立工作空间的面试官,可以使用 MonkeyCode 的免费模型访问和免费服务器选项,同时将隐藏用例套件保持在该工作空间之外。成绩仍来自私有测试,而非生成初稿的工具。

在不使用该工作空间的情况下本文档仍然可用。本地 Node.js 安装和两个测试文件就足够评分。

局限性及不适合使用本文档的人群

本草稿使用固定窗口,而非滑动窗口或带补充速率的令牌桶。固定窗口允许在两个相邻窗口边界处出现短暂突发。在送入计费关键 API 之前不要直接发版,应使用更强的算法和真实共享存储(带原子递增)。

内存存储在跨进程或跨主机场景下不安全。生产环境需要 Redis、DynamDB 或等效方案,能够在不出现读-改-写竞态的情况下递增 key。如果调用方后续针对朴素 async 存储用并发 check 调用包装它,Map 上的参考 incr 也是有竞态的。

对于只需要 HTTP 状态素养的实习筛选跳过本文档。对于不会负责多租户滥用控制的岗位跳过本文档。当面试官无法将隐藏测试文件保持在公开 gist 之外时跳过本文档。不要将公开 fixture 变绿当作 Agent 理解租户的证据。不要在候选者克隆的同一仓库中发布隐藏文件。

如何在面试周运行这个循环

仅发送 prompt 和 public-fixture.test.js,hard constraints 逐字复制。

如果团队已允许工具使用,则允许任何 assistant,包括免费远程工作空间。

收集 limiter.js 以及候选者在公开 fixture 之外编写的任何额外测试。

在候选者看不到的干净 checkout 上运行隐藏隔离、fail-closed 和时钟测试。

用评分表打分,在 debrief 中讨论一种失败模式,而非 README。

有价值的信号在于提交的 key 空间是否编码了 tenant 和 route,以及存储错误时是否拒绝流量。其他一切都是围绕一个要么隔离客户要么不隔离的模块的评注。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
Agent 记忆层测试:六条真正能发现腐化的断言
下一篇
周末 Agent 项目攻略:用permit文件管控写操作