fix(security): apply security hardening recommendations from audit
This commit implements several security enhancements based on the findings of a new security audit report, which has also been added to the documentation. - **Security Headers:** Adds a strict Content-Security-Policy (CSP) and other security headers (X-Content-Type-Options, Referrer-Policy) via Nuxt route rules. - **Production Hardening:** Disables Nuxt DevTools in production environments to reduce the attack surface. - **Mixed Content:** All image assets are now loaded over HTTPS to resolve mixed content issues. - **Tabnabbing:** Secures `window.open` calls by adding `noopener,noreferrer`. - **Configuration:** Updates `.gitignore` to ignore all `.env.*` files. - **Docs:** Adds the full security audit report for reference. - **Build:** Corrects a case-sensitive import path to ensure cross-platform build compatibility.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,7 +20,7 @@ logs
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
# .env.*
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
repomix-output.xml
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string | undefined>("");
|
||||
|
||||
@@ -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({
|
||||
|
||||
188
docs/20251107/security-audit.md
Normal file
188
docs/20251107/security-audit.md
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 使用时:
|
||||
// <UButton v-if="item.demoLink && isSafeHttpUrl(item.demoLink)" :href="item.demoLink" ... />
|
||||
```
|
||||
|
||||
## 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` 调整、文件名大小写修复),并附上验证与回退说明。
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user