资讯动态

Double Secret复盘:Flask会话签名伪造与Jinja2模板注入

发布时间:2026/11/6 4:22:19 来源:尧图企业网站定制
Double Secret 这道题我前后翻了三遍才把整条链路捋顺第一次看的时候只盯着登录框找注入点结果卡了两个小时一无所获第二次换了个角度去看响应头和服务端行为才发现真正的入口藏在会话签名里第三次才意识到题目名字里的 Double 不是随口起的它是明明白白告诉你这里有两层秘密第一层在 cookie 里第二层在模板渲染里。这篇就按我实际复盘时的顺序把 Flask session 的签名机制、secret_key 的生成弱点、伪造 session 的完整脚本以及后半段 Jinja2 模板注入的定位、过滤绕过和命令执行一条一条拆开讲。内容偏实战复盘涉及到的所有操作都建议在你自己的本地靶场或者授权环境里跑不要拿去碰任何不属于你的系统。如果你正在学 Web 安全、想搞明白会话为什么能被伪造模板为什么会变成任意代码执行或者单纯想把一类经典链路的思路固化下来下面的内容应该能省掉你不少翻文档的时间。1. 拿到题目先别急着敲 payload攻击面是怎么一层层摊开的1.1 标题里的 Double 和 Secret 分别指向什么我习惯拿到一个目标先不碰键盘先在纸上把标题拆一遍。这道题叫 Double Secret两个词拆开看Secret 说明整个题的核心围绕某个密钥/秘密展开不是常规的 SQL 注入也不是文件上传Double 则暗示这个秘密是分层的你要先拿到第一层才能看到第二层的入口。这个判断在后续验证里完全对上了第一层秘密是 Flask 应用的签名密钥也就是secret_key第二层秘密是拿到会话控制权之后那个被藏在模板渲染里的执行能力。这种命名不是个例。做过一批题目之后你会发现出题人取名字通常很诚实题名往往就是题解目录。反过来讲如果你看到题名一头雾水多半是你对某一块的认知存在盲区。所以我现在复盘任何一道题第一步都是把题名里的名词逐个查一遍看它们在所属技术栈里对应什么实体。这比漫无目的地在页面上乱点效率高太多了。还有一点值得说这道题从头到尾没有出现任何漏洞提示页面只告诉你需要秘密。这种设计其实是在考信息收集的耐心。我当时的做法是把首页的响应头、cookie、HTML 注释、robots.txt、常见备份路径全部过了一遍最后是在一个看起来平平无奇的路由里看到了服务端代码片段。这一步没有任何技巧就是把常用字典按重要性排个序从最有希望的开始试。1.2 我为什么坚持先找源码再想利用Web 题目里有一条我踩出来的经验能用源码解决的问题就不要靠猜。代码泄露意味着你可以直接看到过滤规则、路由结构、密钥生成方式等于出题人把答案摆在桌上了只差你自己去搬。相反纯黑盒状态下你每一个 payload 都是在赌赌对了不知道为什么对赌错了也不知道错在哪时间全浪费在试错上。这道题的关键转折点就来自一份源码片段。它暴露了三件事路由清单、secret_key的生成方式、以及拿到管理员会话后渲染模板的那一行代码。有了这三样后面全程都是按图施工没有一秒钟是靠感觉走的。我把这个习惯推荐给所有刚开始做这类题的朋友先花十分钟确认有没有代码可看再花十分钟去找剩下的时间才是构造利用。反过来说找源码的时候不要只盯着.git、.svn、www.zip这几个老面孔。我这次的经验是题目作者常常在正常路由后面挂一个调试用的路径比如带src、source、debug、backup之类的关键词而且往往没有任何提示链接指向它。所以目录扫描这一步在 Web 题里永远不能省字典不需要多一两千条常用的就够了重点是跑得勤、看得细别只扫一遍就放弃。提示扫描他人站点属于高风险行为本文所有涉及扫描、爆破的步骤都只在你自己的本地环境执行对外部系统做任何测试前必须有明确的书面授权。2. 第一层 Secret把 Flask session 的签名摊开来看2.1 session cookie 的内部结构到底长什么样很多人看到 Flask 的 session cookie 第一反应是加密的其实它不是加密是签名 明文。默认情况下 Flask 把 session 数据做 JSON 序列化然后做 base64url 编码最后用secret_key做 HMAC 签名把签名也 base64url 编码后拼在点号后面。所以整个过程里数据从来没有被加密过你把 cookie 中间那段解个 base64 就能看到明文的键值对。这个认知非常关键因为它意味着只要你能猜到密钥你就能自己签一份并且服务端完全认不出来。先看一眼典型的 cookie 形态eyJhZG1pbiI6ZmFsc2V9.aBcDeFgHiJkLmNoPqRsTuVwXyZ0点号前面叫 payload 段点号后面是签名段。payload 段解出来就是{admin:false}这样的 JSON。如果数据体积比较大Flask 会先做 zlib 压缩压缩后的 payload 段前面会多出一个点号变成.eJw...这种三段式结构。我第一次把三段式 cookie 当作带版本号的格式去解析脚本直接报错卡了十几分钟才反应过来那是压缩标记这个坑后面会详细说。这里有个细节容易被忽略base64url 编码在 Flask 里是不带等号填充的也就是被剥掉了。你在脚本里解码的时候必须手动补回来否则长度不是 4 的倍数就会抛异常而异常信息通常很难直接指向真正的原因。我在第 5 节会给出处理这个问题的完整函数。2.2 手写签名与验签不依赖第三方工具的做法很多人第一反应是装个现成的工具去做这些事我其实更推荐先手写一遍。原因很实际手写一遍之后你对算法的理解会完全不一样遇到工具报错的时候你能判断是工具问题还是目标问题而不是干瞪眼。下面是我当时写的验签和签名函数纯标准库二十行就能跑。import hmac import hashlib import base64 import json import zlib def b64_encode(data: bytes) - str: return base64.urlsafe_b64encode(data).rstrip(b).decode() def b64_decode(text: str) - bytes: pad * (-len(text) % 4) return base64.urlsafe_b64decode(text pad) def sign_session(payload: dict, secret: str, compress: bool False) - str: raw json.dumps(payload, separators(,, :)).encode() if compress: compressed zlib.compress(raw) payload_part . b64_encode(compressed) data_for_sig b64_encode(compressed) else: payload_part b64_encode(raw) data_for_sig payload_part sig hmac.new(secret.encode(), data_for_sig.encode(), hashlib.sha1).digest() return payload_part . b64_encode(sig) def verify_and_decode(cookie: str, secret: str): if cookie.startswith(.): body, _, sig_part cookie[1:].rpartition(.) raw zlib.decompress(b64_decode(body)) data_for_sig body else: body, _, sig_part cookie.rpartition(.) raw b64_decode(body) data_for_sig body expect b64_encode(hmac.new(secret.encode(), data_for_sig.encode(), hashlib.sha1).digest()) if not hmac.compare_digest(expect, sig_part): return None return json.loads(raw)这段代码里有三个地方必须注意。第一签名计算用的是 payload 段编码后的字符串不是你解码出来的 JSON也不是压缩前的原始字节顺序一旦搞错签名永远对不上。第二压缩状态下参与签名的仍然是 base64 编码后的压缩数据但 cookie 里多出来的那个前导点号不参与签名。第三sha1是 Flask 默认的签名算法有些版本或者自定义配置会换成 sha256遇到签不过的情况先检查这一点。我建议你把这段代码存成一个本地小工具命名为flask_session_tool.py后面遇到任何同类题目直接改两个参数就能用。这类基础能力攒得越多就越不容易被单道题卡住。2.3 secret_key 的生成逻辑为什么经不起推敲正常业务里secret_key应该是一串足够长的随机值从环境变量注入不进代码仓库。但只要是人写的代码就总有人图省事常见的偷懒写法有这么几种直接写死一个字符串、用os.urandom(24)生成、用uuid.uuid4()生成、或者用随机数模块生成再取哈希。前两种在题目里没法直接利用后两种就完全不同了因为它们的随机其实是可复现的。uuid.uuid4()依赖系统的随机源正常情况猜不出来但有些老环境里随机源质量很差理论上存在被推演的可能只不过这道题没走这条路。这道题走的是随机数模块这条路代码里先给随机数生成器一个种子然后基于生成结果做一次哈希当密钥。种子一旦可枚举整个密钥空间就塌掉了。这个概念很好理解随机数生成器本质是一个确定性的数学函数同样的种子喂进去出来的序列永远一样所以随机只是对外部观察者而言的。我当时看到源码里那行种子固定写在代码里心里就一句话这题的第一层已经送分了。但为了把方法论讲完整下面我还是把种子可枚举和种子未知只能爆时间戳两种情况都写清楚因为后者在真实比赛里更常见。3. 密钥怎么拿三条常规路径的取舍与效率测算3.1 字典爆破的适用边界与耗时估算先说最没技术含量的那条路字典爆破。做法就是拿一堆常见弱密钥去尝试签名、拿结果去比对目标 cookie 的签名段对上就是密钥。这条路的价值不在于技术含量而在于它足够快如果你的字典里正好有几秒钟就出结果了根本不用去读源码。什么时候值得先跑字典我的判断标准是如果这个应用看起来像随手搭的就有很大概率用弱密钥。判断方法也不复杂看响应里有没有明显的框架默认特征、看有没有调试页面残留。抛去这些字典爆破的成本其实很低一个几万条的常见口令表加一次 HMAC 运算普通笔记本上一分钟以内就能跑完一轮完全可以作为第一发试探。但要清楚它的天花板。密钥只要稍微长一点、或者带一点业务相关的随机后缀字典就彻底失效这时候继续加大字典只是在浪费时间。我的原则是字典只跑一轮出不来立刻转去读源码绝不在这上面反复追加。时间分配上字典阶段控制在总时长的十分之一以内比较合理。3.2 从生成算法反推种子穷举的具体实现源码是这道题的胜负手。当时泄露的片段大意是应用在启动时用固定的整数作为种子初始化随机数生成器取一次输出转成字符串后做一次哈希结果直接赋给secret_key。这里固定种子意味着完全可复现你只要知道是用哪个随机函数、取了几次、怎么转换的就能一模一样地把密钥算出来。复现的时候有几个必须对齐的点任何一个不一致都算不出来。第一取的是第几个随机数是初始化后立刻取还是中间有别的地方消耗过随机数。第二random()返回浮点数转字符串时是str()还是repr()两者在某些情况下输出不同。第三哈希用的是哪种摘要算法输出是十六进制小写还是大写。我当时第一遍算不出来就是因为源码里在取随机数之前还有一行消耗了随机数导致整体错位一位加了个循环从第 0 到第 5 个依次试就解决了。下面是我用来穷举的脚本把种子和偏移都做成参数方便一次跑完整个猜测空间import random import hashlib def gen_key(seed, skip0, digestmd5): r random.Random(seed) for _ in range(skip): r.random() value str(r.random()) return hashlib.new(digest, value.encode()).hexdigest() for seed in range(0, 3000): for skip in range(0, 6): key gen_key(seed, skip) # 用 key 调上一节的 sign_session 生成 cookie再去比对 print(seed, skip, key)random.Random(seed)这个写法比直接random.seed(seed)更安全因为它开一个独立的实例不会污染全局状态脚本里多处调用也不会互相干扰。这一点在做批量穷举时很重要我早期写脚本的时候用全局种子结果后面加了个随机打乱就把整个序列搞乱了排查了很久。3.3 伪造 session 时的字段设计那些 0、1 和 true 的坑拿到密钥之后下一步是构造 payload。看起来简单——把admin改成true不就行了但这里恰恰是这道题最容易翻车的地方。服务端判断的写法是session.get(admin) is True注意是is True不是 True也不是if session.get(admin)。三种写法对应的伪造值完全不同服务端判断写法可用的伪造值说明is TrueJSON 的true必须是布尔真值1、1、true全部失效 Truetrue、1、1.0松散比较数字 1 也能过if session.get(admin)除 0、空串、null 外的任何值只要为真即可我第一次就是图省事写了admin: 1服务端一直返回拒绝还以为是密钥算错了回头去重算了两遍密钥浪费了不少时间。后来把源码那行判断翻出来逐字读了一遍才反应过来。这个教训很实在伪造字段之前务必把服务端的判断语句原样抄下来看一遍一个is和一个的差别就决定了你构造的值该是布尔还是数字。另外还有个小细节JSON 序列化时的分隔符。Flask 默认序列化是紧凑格式键值对之间没有空格。如果你的脚本用了json.dumps的默认参数会带空格虽然签名正确的前提下服务端解出来仍然是同一个字典但为了减少无关变量建议固定用separators(,, :)让生成的 cookie 和目标格式完全一致便于对照排查。4. 第二层 Secret模板渲染点的定位与利用4.1 怎么确认渲染引擎和注入入口伪造会话成功之后页面终于吐出了新内容但真正的坑才刚开始。这时候页面表现是一个简单的问候语比如Hello, guest而 URL 后面带着一个name参数。看到这种参数值直接显示在页面上的场景第一反应应该是模板注入但必须验证因为反射型 XSS 也会呈现同样的现象两者的区别在于输入是被当作字符串拼接还是被当作模板代码解析。最快的验证方法是输入一个七乘七的算术表达式。如果返回 49说明表达式被求值了那就是模板引擎在解析你的输入如果原样返回{{7*7}}那就只是字符串拼接方向要改。这个测试的好处是它对绝大多数模板引擎都有效而且是无害的不需要任何危险操作就能判断。我一般还会顺手再发一个{{7*7}}因为不同引擎对字符串乘法的处理不一样返回一串 7 还是 7777777 能帮你进一步缩小引擎范围。确认是模板注入之后接下来要摸清楚的是这个引擎暴露了哪些对象。不同版本的模板引擎可访问的内置对象不一样能用的利用链也不同。常规做法是先打印配置对象和应用上下文看看能不能摸到全局命名空间如果能摸到基本就等于拿到了进入命令执行的钥匙。这一步的关键是耐心不要一上来就发最复杂的那条 payload先从最简单的信息泄露试起。4.2 被过滤了怎么写几种绕过思路的实际效果这道题的第二层秘密就在过滤上。页面对一些关键字做了拦截直接用最经典的写法会被挡掉。当时试了几轮最后总结出几条可复用的绕过思路按成功率从高到低排第一条是改属性访问方式。点号在某些过滤规则下会被拦用过滤器去取属性就能绕过去形如{{ x|attr(__class__) }}。过滤器本质上是一个函数调用属性名作为字符串参数传入不带点号自然就不触发拦截。这条思路的通用性极强几乎适用于所有基于黑名单的过滤。第二条是把下划线拆开写。很多规则只匹配连续的__globals__这种完整单词如果你把每个下划线拆成十六进制转义拼出来的字符串在解析时会被还原但过滤规则看到的是另一串东西。实测下来这条对只做字符串匹配的规则非常有效。第三条是换个入口对象。经典链路通常从空字符串或者空元组出发先拿类再顺着继承关系往上爬到基类再从子类列表里找到能执行命令的类。但如果某个环节被拦完全可以换另一条链比如从模板上下文里已有的全局函数出发直接去它的全局命名空间里取内置模块。不同的入口对象到达同一个终点被拦的概率低得多。下面这几条是我在本地环境验证通过的写法供你在自己的靶场里对照{{ config }} {{ .__class__.__mro__[1].__subclasses__() }} {{ ()|attr(__class__)|attr(__base__)|attr(__subclasses__)() }} {{ x|attr(\x5f\x5fclass\x5f\x5f) }}第一条用于快速确认上下文对象是否可读属于探测性质第二条用于枚举可用的子类输出会很长建议在本地看着终端翻后两条是属性访问的替代写法用来验证过滤规则到底拦的是哪个字符。发的时候不要一次性把所有 payload 都塞进请求里一次一个出结果就记录这样排查思路才清晰。注意所有 payload 只在你自己搭建的本地环境中测试。把这类技巧用于未授权的系统性质就完全变了。4.3 拿到执行能力之后该做什么命令执行到手之后很多人的第一反应是去找一个叫flag的文件然后cat一下就完事。这个思路在本地靶场没问题但我想借这道题多说两句思路层面的东西拿到执行权之后真正值得观察的是当前进程能看到什么而不是急着找文件。具体来说值得看的有几类东西。一是环境变量很多应用会把配置项塞在环境变量里里面经常有不该出现的东西。二是当前工作目录和进程启动参数这能告诉你应用是怎么跑起来的、配置文件在哪。三是应用自身的源码因为从源码里你能看到更多路由和隐藏逻辑运气好的话直接就能找到目标。四是网络与文件系统的可达范围这一条在真实环境里最关键也是评估影响面时必须回答的问题。我个人习惯的顺序是先确认自己是谁、在什么目录、有没有权限然后看环境变量最后再按前两步得到的信息去定向查找而不是一上来就全盘搜索。全盘搜索又慢又容易触发告警效率极低。这个习惯在真实工作中帮我省了很多时间也避免了不少误操作。5. 完整复现记录从本地搭建到拿到 flag5.1 本地还原一套最小可用的环境想把整条链路吃透光看别人的 writeup 是不够的必须自己搭一遍。下面是我用来还原这道题最小特征的应用代码把它存成app.py装上框架依赖就能跑。之所以强调最小特征是因为复现的目的是理解机制不是把题目原样搬回来代码越短看的时候干扰越少。import os import random import hashlib from flask import Flask, session, request, render_template_string app Flask(__name__) random_gen random.Random(1337) app.secret_key hashlib.md5(str(random_gen.random()).encode()).hexdigest() app.route(/) def index(): return Only the one who knows the secret can pass. app.route(/secret) def secret(): if session.get(admin) is not True: return Not admin. name request.args.get(name, guest) return render_template_string(Hello, name) if __name__ __main__: app.run(host127.0.0.1, port5000)启动之后访问首页你会拿到一个带着 session cookie 的响应。把这个 cookie 原样记下来后面所有验证都基于它。这里有个容易忽略的点如果你访问一次之后又刷新了页面cookie 可能会变因为 session 内容变化会触发重新签名所以最好在第一次拿到 cookie 时就落到文本文件里避免后面比对时对不上。5.2 分步验证与现场记录第一步先把目标 cookie 拆开看结构。用第 2 节的verify_and_decode密钥随便填一个验证必然失败但你能从报错位置判断出它是两段式还是三段式有没有压缩。这一步纯粹是为了确认格式别看轻它格式判断错了后面全白搭。第二步复现密钥。因为种子写在代码里直接调用gen_key(1337)就能算出来。算出来之后用这个密钥去verify_and_decode目标 cookie如果返回一个字典说明密钥正确。这一步是整个流程里最爽的时刻因为一旦签名验过你就等于拥有了任意构造会话的能力。第三步构造管理员会话。用sign_session({admin: True}, key)生成新 cookie替换掉浏览器里的原值再访问/secret。如果返回从 Not admin. 变成了问候语说明第一层打通。这一步失败的话九成是值类型写错了写成 1 或者字符串回去把判断语句再读一遍。第四步验证模板注入。在name参数里放算术表达式看是否被求值。确认后逐步深入到读配置、枚举子类最后找到可用的执行入口读一个本地文件验证效果。我自己复现时是用读系统版本文件来验证的读到内容就说明执行链路通了。第五步回到题目环境下重复上述流程拿到最终目标。整个过程如果顺利从零开始大概二十到三十分钟如果不顺利时间基本都耗在密钥算法对齐和过滤绕过这两块。这也是我建议先本地复现的原因本地你可以随便改代码、加日志、打印中间值而对着一个黑盒你只能靠猜。6. 踩坑清单与排查速查表6.1 几个反复出现的报错与成因复盘下来我在整条链路上踩的坑其实高度集中来来回回就是那么几类。第一类是 base64 解码报长度错误原因永远是忘了补等号填充-len(text) % 4这个公式记住就行。第二类是签名对不上原因集中在三处参与签名的数据段选错了、压缩标记处理错了、摘要算法用错了。排查办法是拿一个自己签、自己验的 cookie 先跑通确认函数本身没问题再去验目标的。第三类是会话伪造成功但权限没变这类问题最迷惑人因为你会怀疑密钥错了。实际上密钥没错错的是值类型或者字段名。字段名大小写、布尔与数字、嵌套层级任何一个不对都会导致服务端读不到。我的做法是把目标应用在本地跑起来加一行打印把读到的 session 打出来一眼就能看到差别在哪。第四类是模板注入没反应。表现是输入的 payload 原样出现在页面上一个字都没变。这时候先退回最简单的算术表达式如果算术也不生效说明这个位置根本不是渲染点或者输入在进入模板之前被转义了如果算术生效但复杂 payload 不生效那就是过滤在起作用按第 4.2 节的顺序换写法。现象最可能的原因处理方式base64 解码抛异常缺等号填充用-len(s) % 4补齐签名始终不匹配参与签名的数据段选错确认用编码后字符串而非原始字节签名不匹配但逻辑看着都对摘要算法不是 sha1依次尝试 sha1、sha256会话伪造后仍被拒绝值类型不符合is True改为 JSON 布尔true会话伪造后仍被拒绝字段名拼写或大小写不符对照源码逐字核对模板表达式未求值该位置不是渲染点换参数、换路由再测简单表达式生效复杂 payload 不生效存在关键字过滤换属性访问写法或拆解关键字三段式 cookie 解析失败前导点号未剥离先去掉首字符再分割这张表基本上覆盖了这条链路上 90% 的卡点。我的建议是遇到问题先查表别急着从头再来一遍。很多时候你以为需要重做其实只是某一个字符的问题重做反而会让你在同一个地方再摔一次。6.2 几条不太容易从文档里学到的经验第一条是关于时间分配。做这类题最忌讳的是在单点死磕。我的习惯是给每个环节设一个时间上限比如密钥环节二十分钟超了就停下来换思路把当前进展和卡点写在纸上换个角度再回来看。这个做法救过我很多次因为人在一个思路上钻久了会丧失判断力明明该换方向了还在原地打转。第二条是关于记录。我现在做任何复现都会开一个纯文本文件把每一步的请求、响应关键部分、脚本输出都贴进去。看着麻烦但排查的时候价值巨大尤其是当你需要回头确认我到底试过哪个种子的时候。而且这份记录直接就能变成复盘文章的素材一举两得。第三条是关于工具的选择。现成的工具当然好用但我建议每类工具至少自己手写过一次。手写过的部分你在遇到工具失灵时才有能力定位问题。这个道理放到任何技术领域都成立工具是加速器不是替代品你对底层机制的理解深度决定了你能走多远。第四条也是最容易被忽略的一条一定要在本地复现。对着黑盒调 payload你永远不知道服务端到底收到了什么、处理成了什么。本地跑起来加一行日志所有猜测立刻变成确定的事实。花在搭环境上的那半小时通常会给你省下好几个小时。7. 从这道题往外看同类设计在真实系统里的影子7.1 密钥管理上的几个典型误区这道题虽然是个比赛题目但它反映的问题在真实项目里一点都不稀罕。我把见过的密钥管理问题归成三类每一类都值得开发者自查。第一类是把密钥写进代码。这是最常见的也是最危险的因为密钥会跟着代码进入版本库任何能读代码的人都能拿到。正确做法是从环境变量或者专门的配置中心读取代码里只保留读取逻辑并且保证配置文件的权限足够严格。第二类是密钥强度不足。有些项目为了省事直接用项目名、域名、或者短字符串当密钥这类密钥在字典面前基本没有抵抗力。合格的密钥应该是足够长的随机字节人工不可预测也不应该出现在任何文档或聊天记录里。第三类是密钥长期不轮换。严格来说所有密钥都应该有轮换机制尤其是怀疑泄露的时候。但很多项目一用就是几年中间人员变动、环境迁移都不更新等于把风险敞口一直敞着。轮换确实会带来会话失效的成本但这个成本远小于密钥泄露后的损失。提示上面说的这些都属于防御视角的自我检查。如果你在授权测试中发现了类似问题正确做法是写进报告并给出修复建议而不是继续往下深挖。7.2 模板渲染的边界到底该怎么划模板注入的根源其实就一句话把用户输入当成了代码。模板引擎的设计初衷是让开发者写页面它天然拥有很强的表达能力能访问上下文里的对象、能调用方法。一旦用户输入被拼接进模板字符串本身而不是作为变量传进去用户就获得了和开发者同等的表达能力。正确的做法很明确模板文件里写死结构用户输入只作为变量传入。比如问候语应该写成模板里的一个占位符渲染时把名字当参数传进去而不是把名字拼到模板字符串里。这个区别看起来很小实际是天壤之别前者用户输入永远只是数据后者用户输入会先被解析成模板语法。我见过一些团队的处理方式是给用户输入做个过滤把花括号之类的字符替换掉。这种做法的防护效果非常有限因为绕过方式太多了而且随着业务迭代过滤规则很难维护。真正的解法是把数据和代码分开从结构上杜绝这种可能而不是在字符层面打补丁。凡是靠黑名单拦字符的方案本质上都是在赌攻击者不知道某种写法这个赌注迟早会输。最后分享一个我在实际复盘中的小习惯每做完一道题我会问自己三个问题——这道题考的是哪个基础机制这个机制在真实系统里对应什么配置项如果我是防守方最小的改动是什么把这三个问题答完这道题才算真正吃透而不只是拿到了一个结果。Double Secret 这道题我的答案分别是会话签名与模板渲染密钥来源与渲染方式密钥外置到环境变量、用户输入只走变量传入。下次你再遇到名字里带 secret、key、token 这类词的题目不妨先想想这三个问题很多时候方向就出来了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑