con un clic
security-review
Security checklist and best practices for the personal blog project
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Security checklist and best practices for the personal blog project
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
TypeScript and JavaScript coding standards for the personal blog project
Next.js 16 + React 19 + Tailwind CSS best practices and patterns for the personal blog project
Test-driven development methodology and workflow for the personal blog project
| name | security-review |
| description | Security checklist and best practices for the personal blog project |
This skill provides a comprehensive security review checklist for the Next.js + Notion blog.
.env.local file.env.local is in .gitignore// lib/env.ts - Environment variable validation
export function validateEnv() {
const required = ['NOTION_API_KEY', 'NOTION_DATABASE_ID'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
}
// Call in server-side code only
validateEnv();
// ❌ NEVER do this
const API_KEY = 'ntn_abc123...'; // Hardcoded secret
// ❌ NEVER expose in client components
'use client';
const key = process.env.NOTION_API_KEY; // Exposed to browser!
// ✅ CORRECT - Server-side only
// app/api/posts/route.ts
export async function GET() {
const key = process.env.NOTION_API_KEY; // Safe - server-side
// ...
}
// lib/notion.ts - Secure API wrapper
export async function getPosts(): Promise<NotionPost[]> {
try {
const response = await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID!,
filter: {
property: 'Published',
checkbox: { equals: true },
},
});
return response.results.map(mapNotionToPost);
} catch (error) {
// ✅ Log error server-side
console.error('Notion API error:', error);
// ❌ Don't expose internal error details
// throw error;
// ✅ Return safe fallback
return [];
}
}
// lib/rateLimit.ts
const RATE_LIMIT = 3; // requests per second
const cache = new Map<string, number[]>();
export function rateLimit(identifier: string): boolean {
const now = Date.now();
const timestamps = cache.get(identifier) || [];
// Remove timestamps older than 1 second
const recent = timestamps.filter(t => now - t < 1000);
if (recent.length >= RATE_LIMIT) {
return false; // Rate limit exceeded
}
recent.push(now);
cache.set(identifier, recent);
return true;
}
// components/NotionRenderer.tsx
export function NotionRenderer({ blocks }: { blocks: NotionBlock[] }) {
return blocks.map(block => {
switch (block.type) {
case 'paragraph':
// ✅ React automatically escapes text
return <p>{block.paragraph.rich_text[0]?.plain_text}</p>;
case 'code':
// ✅ Use proper code highlighting library
return <SyntaxHighlighter>{block.code.rich_text[0]?.plain_text}</SyntaxHighlighter>;
default:
return null;
}
});
}
// ❌ NEVER use dangerouslySetInnerHTML with unsanitized content
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ If you must use it, sanitize first
import DOMPurify from 'isomorphic-dompurify';
<div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(content)
}} />
Next.js provides built-in CSRF protection for Server Actions.
// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
// ✅ Automatically protected by Next.js
const email = formData.get('email');
// Process form...
}
Not applicable for this project (using Notion API), but if adding a database:
// ✅ Use parameterized queries
const result = await db.query(
'SELECT * FROM posts WHERE slug = $1',
[slug]
);
// ❌ NEVER concatenate user input
const result = await db.query(
`SELECT * FROM posts WHERE slug = '${slug}'` // VULNERABLE!
);
Currently not implemented, but if adding:
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.notion.com",
].join('; '),
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Check for outdated packages
npm outdated
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
next.config.ts// lib/notion.ts - Validate Notion data
function mapNotionToPost(page: any): NotionPost {
// ✅ Validate required fields
if (!page.properties.Title?.title?.[0]?.plain_text) {
throw new Error('Invalid post: missing title');
}
if (!page.properties.Slug?.rich_text?.[0]?.plain_text) {
throw new Error('Invalid post: missing slug');
}
return {
id: page.id,
title: page.properties.Title.title[0].plain_text,
slug: page.properties.Slug.rich_text[0].plain_text,
// ... other fields
};
}
npm audit.env.local not committedIf a security issue is discovered: