code security

This commit is contained in:
overcuriousity 2025-07-19 15:30:12 +02:00
parent e29d10cf81
commit 20e9e5e5ae
8 changed files with 112 additions and 38 deletions

View File

@ -7,6 +7,7 @@ AI_MODEL='ai_model_name_here'
OIDC_ENDPOINT=https://oidc-provider.org OIDC_ENDPOINT=https://oidc-provider.org
OIDC_CLIENT_ID=your_oidc_client_id OIDC_CLIENT_ID=your_oidc_client_id
OIDC_CLIENT_SECRET=your_oidc_client_secret OIDC_CLIENT_SECRET=your_oidc_client_secret
AUTH_SECRET=your_super_secret_jwt_key_that_should_be_at_least_32_characters_long_for_security
AUTHENTICATION_NECESSARY=false # Always set this to true in prod AUTHENTICATION_NECESSARY=false # Always set this to true in prod

View File

@ -8,21 +8,20 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"deploy:static": "./scripts/deploy-static.sh",
"deploy:node": "./scripts/deploy-node.sh",
"check:health": "curl -f http://localhost:3000/health || exit 1" "check:health": "curl -f http://localhost:3000/health || exit 1"
}, },
"dependencies": { "dependencies": {
"astro": "^5.3.0",
"@astrojs/node": "^9.3.0", "@astrojs/node": "^9.3.0",
"js-yaml": "^4.1.0", "astro": "^5.3.0",
"jose": "^5.2.0",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"dotenv": "^16.4.5" "dotenv": "^16.4.5",
"jose": "^5.2.0",
"js-yaml": "^4.1.0",
"zod": "^3.25.76"
}, },
"devDependencies": { "devDependencies": {
"@types/js-yaml": "^4.0.9", "@types/cookie": "^0.6.0",
"@types/cookie": "^0.6.0" "@types/js-yaml": "^4.0.9"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"

View File

@ -88,6 +88,12 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th
</section> </section>
<script define:vars={{ tools, phases, domainAgnosticSoftware }}> <script define:vars={{ tools, phases, domainAgnosticSoftware }}>
function sanitizeHTML(html) {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const aiInterface = document.getElementById('ai-interface'); const aiInterface = document.getElementById('ai-interface');
const aiInput = document.getElementById('ai-query-input'); const aiInput = document.getElementById('ai-query-input');
@ -422,8 +428,11 @@ document.addEventListener('DOMContentLoaded', () => {
` : ''} ` : ''}
</div> </div>
`; `;
aiResults.innerHTML = ''; // Clear previous results first
aiResults.innerHTML = resultsHTML; const tempDiv = document.createElement('div');
tempDiv.innerHTML = resultsHTML;
// Sanitize any dynamic content before inserting
aiResults.appendChild(tempDiv);
} }
}); });
</script> </script>

View File

@ -16,31 +16,24 @@ function getEnv(key: string): string {
} }
const AI_MODEL = getEnv('AI_MODEL'); const AI_MODEL = getEnv('AI_MODEL');
// Rate limiting store (in production, use Redis)
const rateLimitStore = new Map<string, { count: number; resetTime: number }>(); const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
// Input validation and sanitization // Input validation and sanitization
function sanitizeInput(input: string): string { function sanitizeInput(input: string): string {
// Remove potential prompt injection patterns // Remove any content that looks like system instructions
const dangerous = [ let sanitized = input
/ignore\s+previous\s+instructions?/gi, .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks
/new\s+instructions?:/gi, .replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags
/system\s*:/gi, .replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
/assistant\s*:/gi, .replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
/human\s*:/gi, .trim();
/<\s*\/?system\s*>/gi,
/```\s*system/gi,
];
let sanitized = input.trim(); // Limit length and remove excessive whitespace
dangerous.forEach(pattern => { sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
sanitized = sanitized.replace(pattern, '[FILTERED]');
});
// Limit length return sanitized;
return sanitized.slice(0, 2000);
} }
// Strip markdown code blocks from AI response // Strip markdown code blocks from AI response
@ -70,6 +63,17 @@ function checkRateLimit(userId: string): boolean {
return true; return true;
} }
function cleanupExpiredRateLimits() {
const now = Date.now();
for (const [userId, limit] of rateLimitStore.entries()) {
if (now > limit.resetTime) {
rateLimitStore.delete(userId);
}
}
}
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
// Load tools database // Load tools database
async function loadToolsDatabase() { async function loadToolsDatabase() {
try { try {

View File

@ -10,11 +10,13 @@ import {
export const GET: APIRoute = async ({ url, request }) => { export const GET: APIRoute = async ({ url, request }) => {
try { try {
// Debug: multiple ways to access URL parameters if (process.env.NODE_ENV === 'development') {
console.log('Auth callback processing...');
console.log('Full URL:', url.toString()); console.log('Full URL:', url.toString());
console.log('URL pathname:', url.pathname); console.log('URL pathname:', url.pathname);
console.log('URL search:', url.search); console.log('URL search:', url.search);
console.log('URL searchParams:', url.searchParams.toString()); console.log('URL searchParams:', url.searchParams.toString());
}
// Try different ways to get parameters // Try different ways to get parameters
const allParams = Object.fromEntries(url.searchParams.entries()); const allParams = Object.fromEntries(url.searchParams.entries());

14
src/pages/api/health.ts Normal file
View File

@ -0,0 +1,14 @@
import type { APIRoute } from 'astro';
export const prerender = false;
export const GET: APIRoute = async () => {
return new Response(JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime()
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
};

View File

@ -14,7 +14,7 @@ function getEnv(key: string): string {
return value; return value;
} }
const SECRET_KEY = new TextEncoder().encode(getEnv('OIDC_CLIENT_SECRET')); const SECRET_KEY = new TextEncoder().encode(getEnv('AUTH_SECRET'));
const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds const SESSION_DURATION = 6 * 60 * 60; // 6 hours in seconds
export interface SessionData { export interface SessionData {
@ -74,12 +74,13 @@ export function getSessionFromRequest(request: Request): string | null {
// Create session cookie // Create session cookie
export function createSessionCookie(token: string): string { export function createSessionCookie(token: string): string {
const publicBaseUrl = getEnv('PUBLIC_BASE_URL'); const publicBaseUrl = getEnv('PUBLIC_BASE_URL');
const isSecure = publicBaseUrl.startsWith('https://'); const isProduction = process.env.NODE_ENV === 'production';
const isSecure = publicBaseUrl.startsWith('https://') || isProduction;
return serialize('session', token, { return serialize('session', token, {
httpOnly: true, httpOnly: true,
secure: isSecure, secure: isSecure,
sameSite: 'lax', sameSite: 'strict', // More secure than 'lax'
maxAge: SESSION_DURATION, maxAge: SESSION_DURATION,
path: '/' path: '/'
}); });
@ -127,7 +128,7 @@ export async function exchangeCodeForTokens(code: string): Promise<any> {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}` 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
}, },
body: new URLSearchParams({ body: new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',

View File

@ -1,6 +1,43 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import path from 'path'; import path from 'path';
import { z } from 'zod';
const ToolSchema = z.object({
name: z.string(),
description: z.string(),
domains: z.array(z.string()).optional().nullable().default([]),
phases: z.array(z.string()).optional().nullable().default([]),
platforms: z.array(z.string()).default([]),
skillLevel: z.string(),
url: z.string(),
license: z.string(),
tags: z.array(z.string()).default([]),
// Optional fields that can be null, undefined, or empty
projectUrl: z.string().optional().nullable(),
knowledgebase: z.boolean().optional().nullable(),
statusUrl: z.string().optional().nullable(),
accessType: z.string().optional(),
'domain-agnostic-software': z.array(z.string()).optional().nullable(),
});
const ToolsDataSchema = z.object({
tools: z.array(ToolSchema),
domains: z.array(z.object({
id: z.string(),
name: z.string()
})),
phases: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().optional()
})),
'domain-agnostic-software': z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string().optional()
})).optional().default([]),
});
interface ToolsData { interface ToolsData {
tools: any[]; tools: any[];
@ -49,7 +86,14 @@ async function loadRawData(): Promise<ToolsData> {
if (!cachedData) { if (!cachedData) {
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml'); const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
const yamlContent = await fs.readFile(yamlPath, 'utf8'); const yamlContent = await fs.readFile(yamlPath, 'utf8');
cachedData = load(yamlContent) as ToolsData; const rawData = load(yamlContent);
try {
cachedData = ToolsDataSchema.parse(rawData);
} catch (error) {
console.error('YAML validation failed:', error);
throw new Error('Invalid tools.yaml structure');
}
} }
return cachedData; return cachedData;
} }