吾筑之。小应用,或微服务,视之如何耳。以其所为,安从来之?初即本怀。滥施之域,实有之:一应用,纳受公网任意HTML,正招不欲之目者。
初,研设既毕,吾乃专意于结构化之安全检视。吾有安全代理之技,久所砥砺,此诚试之良机。遂命 Codex 以此技为鉴,检视是应用。复申之曰:勿囿于技所覆,倘有他见,吾欲知之。
余文所述,乃详述所标之弱处及所施之缓解之法.
所上传之HTML,可直接于应用之源执行.
此乃亟需关注之关键问题.
事: /api/pages/:id/content返所上传之原始HTML为text/html 自主应用源起。寻常界面后注入CSP,载于沙盒化blob iframe,然攻击者可共享直API URL,绕此隔离。
风险: 同源存储型XSS。虽今应用用户状态微薄,然此可致钓鱼、源滥用、未来权限提升及针对管理员之攻击。
建议之修正: 加设HTTP层防护于内容响应:至少须有Content-Security-Policy与sandbox allow-scripts,并辅以上传页面的CSP,及X-Content-Type-Options: nosniff。或可返内容为附件,或移不可信内容至别源。
应此之道,首在确保页面路由涵括下列标头:
Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
Permissions-Policy = "camera=(), geolocation=(), microphone=(), payment=(), usb=()"
Referrer-Policy = "no-referrer"
X-Content-Type-Options = "nosniff"
注:權限政策向未廣泛可用,故截至本文撰寫之時,五月十五日,二零二六,僅影響基於Chromium之瀏覽器。
getPageContent函數現返回HTML,並設置以下標頭:
{
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store",
"X-Robots-Tag": "noindex",
"Content-Security-Policy": buildUploadedPageHttpCsp(),
"X-Content-Type-Options": "nosniff",
}
關於內容安全政策,吾人始於:
`sandbox allow-scripts; ${buildUploadedPageCsp()}`;
復加buildUploadedPageCsp之結果:
return [
"default-src 'none'",
`script-src ${scripts}`,
`style-src ${styles}`,
"font-src https://fonts.gstatic.com",
"img-src data: blob:",
"media-src data: blob:",
"object-src 'none'",
"base-uri 'none'",
"form-action 'none'",
].join("; ");
script-src 与 style-src 之设,'unsafe-inline' 之启,许以下列之内容递送网络:
const TRUSTED_CDN_ORIGINS = [
"https://cdn.jsdelivr.net",
"https://unpkg.com",
"https://cdnjs.cloudflare.com",
] as const;
。此列中,惟 fonts.googleapis.com 得入 style-src,俾 CSS 得载,而 https://fonts.gstatic.com 则明许于字型文件自身。二者皆非脚本之源。
。亦须谨记,所上传之页,乃于沙盒中呈现。iframe:
<iframe
id="page-iframe"
class="page-iframe"
sandbox="allow-scripts"
title="${`Shared ephemeral page ${pageId}`}"
></iframe>
中危之患
复有中危之患数事待解:
无上传/举报之限
患状:凡人可屡上传二兆字节之页,并屡举报之。
患险:存储之费滥,功能之召滥,Netlify Forms之扰,及审核之乱。
所拟之策:为上传/举报之行添加速率限制或配额,善者依IP或用户代理指纹于边缘或功能层施行。待滥行确凿,方可思CAPTCHA/Turnstile,以避用户之烦。
首议者速率限制也。凡调用应限速率之途,此即首验:此请可否允行?
偶有所思。速率限制之理,其一部分基于RATE_LIMIT_SECRET——此乃以OpenSSL所生之应用密钥,经Netlify之敏感环境变量而示于应用。此密钥之源,藏于1Password之库,虽于本地开发亦严加守护;其读自1Password,借Varlock而得,未尝以明文存于.env之文件也。
检核速率之限,实为迂回之途,愿君稍待。首务当取速率之秘钥。若系统不能取之,则戛然而止,众行者皆无由进。吾将受警于Sentry,以特定之rate_limit_secret_missing事件为凭。倘诸事配置得宜,则此阻隔当永无由现。
欲验速率限制之有无,须先备数事:
- 吾等之秘钥
- 角色哈希
- 主体哈希
- 密钥
- 策略之类型
- 既有之记录
吾知吾有密钥,故须计算吾之哈希。哈希之由,在于安危与隐秘——吾不欲存此为明文,尤以构成角色哈希之要素为甚。
函数hashValue取二参数:value与secret:二哈希之中,密钥皆同此限速之密。至于行者之哈希,其值乃用户IP与用户代理之合。
:于函数之内,首呼crypto.subtle.importKey,以化吾密为CryptoKey,俾可与Web Crypto API相用:
// importKey(format, keyData, algorithm, extractable, keyUsages)
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
:吾既明其素形,乃化吾密自文字为Uint8Array 之用,TextEncoder.encode 以为之设,extractable 乃为 false,而用之限于署名。若诸事顺遂,今已得吾之 CryptoKey。
次第,乃 crypto.subtle.sign:
// sign(algorithm, key, data)
const signature = await crypto.subtle.sign(
"HMAC",
key,
new TextEncoder().encode(value),
);
吾等既定其术,以吾之 CryptoKey 付之,复用 TextEncoder,化吾值于 Uint8Array — 形式也。sign 期之。其果为 ArrayBuffer。继而化之为 Uint8Array,每八位各缀以两字之十六进制符(不足者前置零以补),而合之:
return Array.from(new Uint8Array(signature), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
。如是得字符串若 "0aff0380..." — 此即吾之哈希也。
警之。:吾亦以为,此散列之记,不宜久存。犹若其页,每时辰必有一cron,删去已过之期者。一记永不出于Blob之库,过其有用之时。
:既得吾二散列,次乃取key。
function rateLimitKey(name: RateLimitName, actorHash: string, subjectHash: string): string {
return `${RATE_LIMIT_PREFIX}/${name}/${actorHash}/${subjectHash}.json`;
}
初见似为文件路径。然实为存速率限制条目时所用之钥。Netlify Blobs(Netlify块)斜杠界定乃有意为之,盖因得列某前缀下所有条目——譬如,store.list({ prefix: "rate-limits/" })句首之词,未完。.json扩展仅乃习俗耳。
末三者较为直白。其策有三可能——upload,report,或failedDelete— 定于一常数之中RATE_LIMITS其言明每窗容试之数,及窗之长短。
吾辈乃尝试以钥索现有之速率限制记录,将所得之果,递与之。activeRecord:
function activeRecord(
existing: RateLimitRecord | null,
now: number,
windowMs: number,
): RateLimitRecord {
if (!existing || existing.resetAt <= now) {
return { count: 0, resetAt: now + windowMs };
}
return existing;
}
若无记录存焉,或窗口已过,则新启之。否则,用旧记录。自此,验之甚明:若尝试之数将逾其限,则阻其请,并录Sentry之事件。若非,则更新记录,许其请通过。
管理员删除令无蛮力之防
事端:删除之端点,纳无限承载令牌之尝试,行直截字符串相较。
风险:令牌脆弱或泄,可遭屡次攻伐。
潜在之修正:须求长随机令牌,限速失败之删除,录失败之尝试,且于可行处用恒时相较。
删除之符循同法于限速之秘:以OpenSSL生之,藏于1Password,未尝入源码之控,复经Netlify之秘境示于服务器,其秘境标为机要。
如前所述,页面与API路由均已实施速率限制,失败尝试则记录于Sentry。余下之项——常数时间比较——乃吾所接受之暂时风险。其他缓解措施既已完备,直接字符串比较似可接受。若君有异见,请示知。
注吾亦赖之Netlify 之速率限制 以护平台之安全。
管理者审核,可纳跨源标记之URL。
疑事:pageIdFromUrl 自URL之源,提取/p/:id。
危险:伪造之报如https://evil.example/p/real-id,若评审者信此流程,则可令管理界面授权删除真实之本地页ID。
治:需url.origin === window.location.origin方得启删。
所治者,惟于getIdFromUrl稍增一语耳:
if (url.origin !== window.location.origin) {
return null;
}
若所从来者不类,则函数返null,余者之管束者,谨防虚pageId,而后乃系诸事。
卑/固守之
报二事于此.
主应用缺全域安全头.
问题: 无CSP,frame-ancestors,Referrer-Policy,或Permissions-Policy未于应用壳或管理界面配置.
建议之策: 加Netlify头。尤宜用frame-ancestors 'none' 以御点击劫持,且于可信应用壳中设紧密之CSP。
如前所述,应用壳今行以下标题于全域:
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'"
Permissions-Policy = "camera=(), geolocation=(), microphone=(), payment=(), usb=()"
Referrer-Policy = "no-referrer"
X-Content-Type-Options = "nosniff"
所受HTML或后败CSP注入
疑案:服务器验证容有仅具文体的HTML文书,而 injectCsp 则需 <html> 或 <head>。
风险:部分上传内容呈现为应用错误。
修正:使验证与呈现要求相符,或以解析/序列化方式注入,而非正则表达式。
此显我已不安之虑。虽提交之HTML有验证,然似非定规。吾严整之。今提交之HTML须经数步:首确吾得值,其为字符串,且去空后非空。次验其总字节不超过二兆之限。终乃解析文档。解析5库,并验其果含作者所供html或head之素。惟其如是,乃可继以上传。
结论
细察此事,实为值得。其间显露出真问题——如同源XSS之危——,当亟治之。或犹有未逮,此念将萦绕于心。然此应用经此一番功夫,已显著增其固。












