diff --git a/.env.sample b/.env.example similarity index 100% rename from .env.sample rename to .env.example diff --git a/.gitignore b/.gitignore index d29ccc6..99b3c47 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ logs # Local env files .env -# .env.* +.env.* !.env.example repomix-output.xml \ No newline at end of file diff --git a/app/composables/WhatsAppMsgSender.ts b/app/composables/WhatsAppMsgSender.ts index 106df7d..61b2d78 100644 --- a/app/composables/WhatsAppMsgSender.ts +++ b/app/composables/WhatsAppMsgSender.ts @@ -8,7 +8,7 @@ const _useWhatsAppMsgSender = () => { const sendMessage = (message: string) => { const text = encodeURIComponent(message); const url = `https://api.whatsapp.com/send?phone=${phone}&text=${text}`; - window.open(url, "_blank"); + window.open(url, "_blank", "noopener,noreferrer"); }; return { diff --git a/app/pages/index.vue b/app/pages/index.vue index 15aee9e..b912771 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -80,16 +80,16 @@ useSeoMeta({ const colorMode = useColorMode(); const backgroundImages = [ - "http://img.tootaio.com/i/2025/11/05/avc5ld.png", - "http://img.tootaio.com/i/2025/11/05/avcaff.png", - "http://img.tootaio.com/i/2025/11/05/avcjbw.png", - "http://img.tootaio.com/i/2025/11/05/avcp16.png", - "http://img.tootaio.com/i/2025/11/05/avcv1q.png", - "http://img.tootaio.com/i/2025/11/05/avd47a.png", - "http://img.tootaio.com/i/2025/11/05/avdx6a.png", - "http://img.tootaio.com/i/2025/11/05/avegxy.png", - "http://img.tootaio.com/i/2025/11/05/avemgn.png", - "http://img.tootaio.com/i/2025/11/05/avf3wl.png", + "https://img.tootaio.com/i/2025/11/05/avc5ld.png", + "https://img.tootaio.com/i/2025/11/05/avcaff.png", + "https://img.tootaio.com/i/2025/11/05/avcjbw.png", + "https://img.tootaio.com/i/2025/11/05/avcp16.png", + "https://img.tootaio.com/i/2025/11/05/avcv1q.png", + "https://img.tootaio.com/i/2025/11/05/avd47a.png", + "https://img.tootaio.com/i/2025/11/05/avdx6a.png", + "https://img.tootaio.com/i/2025/11/05/avegxy.png", + "https://img.tootaio.com/i/2025/11/05/avemgn.png", + "https://img.tootaio.com/i/2025/11/05/avf3wl.png", ]; const currentBgImage = ref(""); diff --git a/content.config.ts b/content.config.ts index 49ac46c..91aa2b3 100644 --- a/content.config.ts +++ b/content.config.ts @@ -1,5 +1,5 @@ import { defineContentConfig, defineCollection, z } from "@nuxt/content"; -import { PricingPlanPropsSchema } from "./app/schemas/PricingPlanSchema"; +import { PricingPlanPropsSchema } from "./app/schemas/pricingPlanSchema"; const defineIndexSchema = () => z.object({ diff --git a/docs/20251107/security-audit.md b/docs/20251107/security-audit.md new file mode 100644 index 0000000..3ae2308 --- /dev/null +++ b/docs/20251107/security-audit.md @@ -0,0 +1,188 @@ +--- +title: 安全审计报告(Tootaio Studio 网站) +description: 开源发布前的安全评估与加固建议与清单 +lastUpdated: 2025-11-07 +--- + +# 概览(Summary) + +- 架构为 Nuxt 4 + @nuxt/content 的前端站点,无自建服务端 API/上传功能,攻击面小。若开源,整体安全风险低。 +- 主要关注点:外部资源使用 HTTP、窗口打开的 `opener` 风险、生产环境 DevTools 启用、缺少统一安全响应头、依赖冗余与未来内容来源治理。 + +## 审计范围 + +- 配置与依赖:`nuxt.config.ts`、`package.json`、`.env*`、`.gitignore`。 +- 前端页面与可执行逻辑:`app/` 下的页面、布局、可组合函数(composables)。 +- 内容与多语言:`content/**.yml` 与 `i18n/**.json`。 +- 文档与脚本:`docs/` 内建议信息。 + +# 结论与优先级 + +- 风险等级:低-中(Low–Medium)。不存在明显的密钥泄露或远程代码执行面。 +- 高价值、低成本修复项(建议开源前完成): + 1) 将所有外部资源统一为 HTTPS。 + 2) 生产环境禁用 DevTools。 + 3) `window.open` 显式使用 `noopener,noreferrer`。 + 4) 为外链/图片增加基础协议白名单校验(http/https)。 + 5) 通过网关或 Nitro `routeRules` 添加基础安全响应头与静态资源缓存策略。 + 6) 精简未使用依赖(如未使用 `better-sqlite3`)。 + +# 关键发现与证据(Evidence) + +- 公开运行时配置(非敏感): + - `nuxt.config.ts:22` 暴露 `runtimeConfig.public.whatsappNumber`(按设计公开,无敏感性)。 + +- DevTools 在生产可能启用: + - `nuxt.config.ts:14` `devtools: { enabled: true }`,建议生产禁用。 + +- 外链/图片使用了 HTTP(混合内容与篡改风险): + - `app/pages/index.vue:83-92` 背景图 `http://img.tootaio.com/...` + - `content/en-US/index.yml:25,29` 与 `content/zh-CN/index.yml:31,35` 项目图片使用 `http://`。 + +- 外链打开策略: + - `app/pages/index.vue:37-38` 使用 `target="_blank"` 已含 `rel="noopener"`,建议补充 `noreferrer`。 + - `app/composables/WhatsAppMsgSender.ts:11` 的 `window.open` 未显式 `noopener,noreferrer`。 + +- 统一安全头缺失: + - 未在 `nuxt.config.ts` 配置 `routeRules` 的安全头(可在网关/Nginx 层或 Nitro 层补充)。 + +- 依赖冗余: + - `package.json:19` 引入了 `better-sqlite3`,当前项目未使用,建议移除以降低供应链与构建复杂度。 + +- 非安全但会影响构建的细节: + - `content.config.ts:2` 大小写引用与实际文件名不一致(Linux 下可能报错):应从 `./app/schemas/PricingPlanSchema` 调整为 `./app/schemas/pricingPlanSchema`。 + +- 环境文件治理: + - `.env` 已被 `.gitignore` 忽略(`.gitignore:22`),且内容仅有公开号码(`.env:1`)。建议继续保持从未提交到历史。 + +# 修复与加固建议(Actionable) + +## 开源前必须(High Priority) + +- 统一使用 HTTPS 资源 + - 将 `http://img.tootaio.com/...` 统一替换为 `https://img.tootaio.com/...`。 + - 建议将资源基址抽离到 `runtimeConfig.public.assetBase` 并集中管理,减少散落硬编码。 + +- 生产禁用 DevTools + - 将 `devtools` 改为:`devtools: { enabled: process.env.NODE_ENV !== 'production' }`。 + +- `window.open` 加固 + - 修改为:`window.open(url, '_blank', 'noopener,noreferrer')` 或在新窗口上设置 `opener = null`。 + +- 外链与图片的协议白名单 + - 在渲染外链/图片前校验 URL 协议,只允许 `http:` 与 `https:`,避免 `javascript:`、`data:` 等危险 scheme。 + +- 安全响应头(建议由网关/Nginx 配置,或用 Nitro routeRules) + - 最小集: + - `Content-Security-Policy`(仅放行必要域名,样例见下)。 + - `X-Content-Type-Options: nosniff` + - `Referrer-Policy: strict-origin-when-cross-origin` + - `Permissions-Policy`(按需收紧) + - 静态资源缓存:`/_nuxt/**` 设置 `Cache-Control: public, max-age=31536000, immutable`。 + +## 建议完成(Medium Priority) + +- 依赖精简 + - 若未用到 `better-sqlite3`,从 `package.json` 移除并更新锁文件。 + +- `.gitignore` 更严格忽略 env 变体 + - 将当前注释掉的 `.env.*` 忽略规则启用,并保留示例白名单: + - 忽略:`.env`、`.env.*` + - 白名单:`!.env.sample` + +- 文件名大小写一致性 + - `content.config.ts` 引用改为实际文件名大小写以避免跨平台问题。 + +## 可选增强(Nice-to-have) + +- 在 CI 中启用: + - `pnpm install --frozen-lockfile` + - 依赖与漏洞审计(Dependabot / `pnpm audit --prod`) + - Secrets 扫描(`gitleaks`/`trufflehog`) + +- HSTS 与全站 HTTPS + - 前置网关开启 HSTS,并确保所有外链与资源均可通过 HTTPS 访问。 + +# 附录:建议配置片段 + +## 1) Nitro routeRules(示例) + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + // ... + routeRules: { + '/**': { + headers: { + 'Content-Security-Policy': [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' https://img.tootaio.com data:", + "connect-src 'self'", + "frame-ancestors 'self'", + 'upgrade-insecure-requests', + ].join('; '), + 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + }, + }, + '/_nuxt/**': { + headers: { 'cache-control': 'public, max-age=31536000, immutable' }, + }, + }, +}) +``` + +请按实际依赖域名精简 CSP,特别是 `img-src`、`connect-src`。 + +## 2) `window.open` 安全用法(示例) + +```ts +const win = window.open(url, '_blank', 'noopener,noreferrer') +// 或者: +const win = window.open(url, '_blank') +if (win) win.opener = null +``` + +## 3) 外链协议白名单(示意) + +```ts +function isSafeHttpUrl(href: string) { + try { + const u = new URL(href) + return u.protocol === 'http:' || u.protocol === 'https:' + } catch { + return false + } +} + +// 使用时: +// +``` + +## 4) DevTools 生产禁用 + +```ts +export default defineNuxtConfig({ + devtools: { enabled: process.env.NODE_ENV !== 'production' }, +}) +``` + +## 5) `.gitignore` 建议 + +```gitignore +# Local env files +.env +.env.* +!.env.sample +``` + +# 附注(Non-security 但建议修复) + +- `content.config.ts:2` 的大小写引用问题:应改为 `./app/schemas/pricingPlanSchema`(与实际文件名一致),避免在大小写敏感的文件系统上构建失败。 + +# 下一步 + +- 如需,我可以基于本报告直接提交最小化补丁(HTTPS 资源替换、`devtools` 切换、`window.open` 加固、`routeRules` 安全头、`.gitignore` 调整、文件名大小写修复),并附上验证与回退说明。 + diff --git a/nuxt.config.ts b/nuxt.config.ts index 771dc49..da2bf1b 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -11,7 +11,7 @@ const DEFAULT_SEO = { // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: "2025-07-15", - devtools: { enabled: true }, + devtools: { enabled: process.env.NODE_ENV !== "production" }, modules: [ "@nuxt/content", "@nuxt/ui", @@ -24,6 +24,26 @@ export default defineNuxtConfig({ whatsappNumber: "+601234567890", }, }, + routeRules: { + "/**": { + headers: { + "Content-Security-Policy": [ + "default-src 'self'", + "script-src 'self'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' https://img.tootaio.com data:", + "connect-src 'self'", + "frame-ancestors 'self'", + "upgrade-insecure-requests", + ].join("; "), + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "strict-origin-when-cross-origin", + }, + }, + "/_nuxt/**": { + headers: { "cache-control": "public, max-age=31536000, immutable" }, + }, + }, css: ["@/assets/css/main.css"], app: { head: {