-
Notifications
You must be signed in to change notification settings - Fork 56
fix(security): harden Monolith against XSS, SSRF, info leaks, and abuse #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ dist/ | |
| # 环境变量 | ||
| .env | ||
| .env.local | ||
| .env.* | ||
| !.env.example | ||
| .dev.vars | ||
|
|
||
| # 系统文件 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # 隐私政策 / Privacy Policy | ||
|
|
||
| 最后更新 / Last updated: 2026-04-19 | ||
|
|
||
| --- | ||
|
|
||
| ## 数据收集声明 | ||
|
|
||
| Monolith 博客在您访问时可能自动收集以下非个人身份信息: | ||
|
|
||
| | 数据类型 | 来源 | 用途 | 存储方式 | | ||
| |---------|------|------|---------| | ||
| | 访问页面路径 | 请求 URL | 内容统计 | 边缘数据库 | | ||
| | 访客来源国家 | Cloudflare `CF-IPCountry` 头 | 流量分析 | 边缘数据库 | | ||
| | 来源域名 | `Referer` 头 | 流量分析 | 边缘数据库 | | ||
| | 设备类型 | `User-Agent` 解析(仅区分 desktop/mobile/bot) | 响应式优化 | 边缘数据库 | | ||
| | 评论者昵称 | 用户主动填写 | 公开展示 | 边缘数据库 | | ||
|
|
||
| **我们不收集:** IP 地址(仅处理为不可逆哈希用于投票去重)、邮箱地址不在公开 API 中返回。 | ||
|
|
||
| ## Cookie 使用 | ||
|
|
||
| | Cookie | 用途 | 持续时间 | | ||
| |--------|------|---------| | ||
| | 认证 Token | 管理员登录 | 7 天 | | ||
| | `_gdpr_consent` | 记录隐私同意 | 1 年 | | ||
|
|
||
| 站点管理员可通过"扩展与注入"设置添加第三方分析脚本(如 Google Analytics)。此类脚本可能设置额外的 Cookie,需遵守本政策的同意机制。 | ||
|
|
||
| ## 第三方脚本同意机制 | ||
|
|
||
| 当站点配置了自定义头部/尾部脚本(如分析服务),访客将看到 Cookie 同意横幅。**在您明确同意之前,第三方脚本不会被加载。** 您可以随时撤回同意。 | ||
|
|
||
| ## 数据存储与处理 | ||
|
|
||
| - 所有数据存储在 Cloudflare 边缘网络(Workers + D1/R2),位于就近的数据中心 | ||
| - 评论者邮箱仅用于管理员审核通知,不会在他的评论旁公开显示 | ||
| - 我们不售卖、共享或传输用户数据给第三方 | ||
|
|
||
| ## 数据删除请求 | ||
|
|
||
| 如您希望删除您的评论或相关数据,请通过以下方式联系: | ||
|
|
||
| - GitHub Issue(公开请求) | ||
| - [GitHub Security Advisories](https://github.com/one-ea/Monolith/security/advisories/new)(私密请求) | ||
|
|
||
| 我们将在 14 个工作日内处理您的请求。 | ||
|
|
||
| ## 政策更新 | ||
|
|
||
| 本隐私政策可能不定期更新,重大变更将在博客页面上公告。继续访问本站即表示您同意本政策。 | ||
|
|
||
| --- | ||
|
|
||
| ## Data Collection Notice | ||
|
|
||
| Monolith blog may automatically collect the following non-personally identifiable information when you visit: | ||
|
|
||
| | Data Type | Source | Purpose | Storage | | ||
| |-----------|--------|---------|---------| | ||
| | Page path visited | Request URL | Content analytics | Edge database | | ||
| | Visitor country | Cloudflare `CF-IPCountry` header | Traffic analysis | Edge database | | ||
| | Referrer domain | `Referer` header | Traffic analysis | Edge database | | ||
| | Device type | `User-Agent` parsing (desktop/mobile/bot only) | Responsive optimization | Edge database | | ||
| | Commenter nickname | User-provided | Public display | Edge database | | ||
|
|
||
| **We do not collect:** IP addresses (only hashed for vote deduplication), email addresses are never returned in public APIs. | ||
|
|
||
| ## Cookie Usage | ||
|
|
||
| | Cookie | Purpose | Duration | | ||
| |--------|---------|----------| | ||
| | Auth Token | Admin login | 7 days | | ||
| | `_gdpr_consent` | Records privacy consent | 1 year | | ||
|
|
||
| Site administrators may add third-party analytics scripts via "Extensions & Injection" settings. Such scripts may set additional cookies and are subject to the consent mechanism described in this policy. | ||
|
|
||
| ## Third-Party Script Consent | ||
|
|
||
| When custom header/footer scripts (e.g., analytics) are configured, visitors will see a cookie consent banner. **Third-party scripts are not loaded until you explicitly consent.** You may withdraw consent at any time. | ||
|
|
||
| ## Data Storage & Processing | ||
|
|
||
| - All data is stored on the Cloudflare edge network (Workers + D1/R2) in nearest data centers | ||
| - Commenter emails are used only for admin review notifications and are never displayed publicly | ||
| - We do not sell, share, or transfer user data to third parties | ||
|
|
||
| ## Data Deletion Requests | ||
|
|
||
| To request deletion of your comments or related data, please contact us via: | ||
|
|
||
| - GitHub Issue (public request) | ||
| - [GitHub Security Advisories](https://github.com/one-ea/Monolith/security/advisories/new) (private request) | ||
|
|
||
| We will process your request within 14 business days. | ||
|
|
||
| ## Policy Updates | ||
|
|
||
| This privacy policy may be updated periodically. Major changes will be announced on the blog. Continued use of this site constitutes acceptance of this policy. | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,22 +10,10 @@ function formatDate(dateStr: string): string { | |
| }); | ||
| } | ||
|
|
||
| /** 通过邮箱生成 Gravatar 头像 URL */ | ||
| function gravatarUrl(email: string, size = 40): string { | ||
| // 简单 hash 用于无邮箱时的默认颜色 | ||
| if (!email) return `https://api.dicebear.com/7.x/initials/svg?seed=U&size=${size}`; | ||
| const trimmed = email.trim().toLowerCase(); | ||
| return `https://gravatar.com/avatar/${simpleHash(trimmed)}?s=${size}&d=identicon`; | ||
| } | ||
|
|
||
| function simpleHash(str: string): string { | ||
| let hash = 0; | ||
| for (let i = 0; i < str.length; i++) { | ||
| const char = str.charCodeAt(i); | ||
| hash = ((hash << 5) - hash) + char; | ||
| hash |= 0; | ||
| } | ||
| return Math.abs(hash).toString(16).padStart(32, "0"); | ||
| /** 通过昵称生成 DiceBear 头像 URL(不再传输邮箱) */ | ||
| function avatarUrl(name: string, size = 40): string { | ||
| const seed = encodeURIComponent(name.trim() || "U"); | ||
| return `https://api.dicebear.com/7.x/initials/svg?seed=${seed}&size=${size}`; | ||
| } | ||
|
Comment on lines
+13
to
17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 头像请求会把访客信息泄漏给第三方 DiceBear 本 PR 主线强调 GDPR 合规并为第三方脚本加了 Cookie 同意门禁;但此处每条评论的 建议二选一:
方案 1 顺带省掉一次 HTTPS 握手,移动端性能也更好。 🤖 Prompt for AI Agents |
||
|
|
||
| /* ── 单条评论 ──────────────────────────── */ | ||
|
|
@@ -34,7 +22,7 @@ function CommentItem({ comment }: { comment: CommentData }) { | |
| <div className="group flex gap-[12px] py-[16px]"> | ||
| <div className="shrink-0"> | ||
| <img | ||
| src={gravatarUrl(comment.authorEmail)} | ||
| src={avatarUrl(comment.authorName)} | ||
| alt={comment.authorName} | ||
| className="h-[36px] w-[36px] rounded-full bg-card/30 ring-1 ring-border/20" | ||
| loading="lazy" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { useEffect, useState } from "react"; | ||
|
|
||
| const CONSENT_KEY = "_gdpr_consent"; | ||
| const CONSENT_EXPIRY_DAYS = 365; | ||
|
|
||
| function getConsent(): "accepted" | "rejected" | null { | ||
| try { | ||
| const raw = localStorage.getItem(CONSENT_KEY); | ||
| if (!raw) return null; | ||
| const data = JSON.parse(raw); | ||
| if (Date.now() > data.expires) { | ||
| localStorage.removeItem(CONSENT_KEY); | ||
| return null; | ||
| } | ||
| return data.value as "accepted" | "rejected"; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function setConsent(value: "accepted" | "rejected") { | ||
| localStorage.setItem(CONSENT_KEY, JSON.stringify({ | ||
| value, | ||
| expires: Date.now() + CONSENT_EXPIRY_DAYS * 86400000, | ||
| })); | ||
| } | ||
|
|
||
| export function getCookieConsent(): boolean { | ||
| return getConsent() === "accepted"; | ||
| } | ||
|
|
||
| export function CookieConsent() { | ||
| const [visible, setVisible] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| setVisible(getConsent() === null); | ||
| }, []); | ||
|
|
||
| if (!visible) return null; | ||
|
|
||
| return ( | ||
| <div className="fixed bottom-0 left-0 right-0 z-[9999] p-[12px] animate-fade-in"> | ||
| <div className="mx-auto max-w-[720px] rounded-xl border border-border/30 bg-card/95 backdrop-blur-md shadow-lg shadow-black/20 px-[20px] py-[16px] flex flex-col sm:flex-row items-start sm:items-center gap-[12px]"> | ||
| <div className="flex-1 text-[13px] text-muted-foreground/80 leading-[1.6]"> | ||
| 本站使用 Cookie 进行访问统计与第三方脚本加载。继续访问即表示您同意我们的{" "} | ||
| <a href="/privacy" className="text-foreground/70 underline underline-offset-2 hover:text-foreground transition-colors">隐私政策</a>。 | ||
| </div> | ||
| <div className="flex gap-[8px] shrink-0"> | ||
| <button | ||
| onClick={() => { setConsent("rejected"); setVisible(false); }} | ||
| className="px-[14px] py-[6px] text-[12px] rounded-md border border-border/30 text-muted-foreground/60 hover:text-foreground hover:border-border/50 transition-all" | ||
| > | ||
| 拒绝 | ||
| </button> | ||
| <button | ||
| onClick={() => { setConsent("accepted"); window.dispatchEvent(new Event("cookie-consent-accepted")); setVisible(false); }} | ||
| className="px-[14px] py-[6px] text-[12px] rounded-md bg-foreground text-background font-medium hover:bg-foreground/80 transition-all" | ||
| > | ||
| 接受 | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
Comment on lines
+41
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 建议增强无障碍标记、SPA 导航与错误容错
♻️ 建议修改-import { useEffect, useState } from "react";
+import { useEffect, useState } from "react";
+import { Link } from "wouter";
@@
function setConsent(value: "accepted" | "rejected") {
- localStorage.setItem(CONSENT_KEY, JSON.stringify({
- value,
- expires: Date.now() + CONSENT_EXPIRY_DAYS * 86400000,
- }));
+ try {
+ localStorage.setItem(CONSENT_KEY, JSON.stringify({
+ value,
+ expires: Date.now() + CONSENT_EXPIRY_DAYS * 86400000,
+ }));
+ } catch {
+ /* 存储不可用时静默失败,横幅仍需关闭 */
+ }
}
@@
- <div className="fixed bottom-0 left-0 right-0 z-[9999] p-[12px] animate-fade-in">
+ <div
+ className="fixed bottom-0 left-0 right-0 z-[9999] p-[12px] animate-fade-in"
+ role="region"
+ aria-label="Cookie 同意"
+ >
@@
- <a href="/privacy" className="text-foreground/70 underline underline-offset-2 hover:text-foreground transition-colors">隐私政策</a>。
+ <Link href="/privacy" className="text-foreground/70 underline underline-offset-2 hover:text-foreground transition-colors">隐私政策</Link>。根据代码规范:“无障碍访问(aria 标签、键盘导航)”、“响应式布局”。 🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
数据收集表未列出"评论者邮箱"条目
第 19 行声明邮箱"不在公开 API 中返回",且第 37 行说明邮箱用于管理员通知,说明系统确实收集并存储邮箱。为符合 GDPR 的透明性要求,建议在数据收集表中显式列出"评论者邮箱"一项(来源:用户主动填写;用途:管理员审核通知;存储:边缘数据库,不公开),避免用户误以为未收集。英文版同理(第 59-67 行)。
🤖 Prompt for AI Agents