code security
This commit is contained in:
		
							parent
							
								
									e29d10cf81
								
							
						
					
					
						commit
						20e9e5e5ae
					
				@ -7,6 +7,7 @@ AI_MODEL='ai_model_name_here'
 | 
			
		||||
OIDC_ENDPOINT=https://oidc-provider.org
 | 
			
		||||
OIDC_CLIENT_ID=your_oidc_client_id
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								package.json
									
									
									
									
									
								
							@ -8,21 +8,20 @@
 | 
			
		||||
    "build": "astro build",
 | 
			
		||||
    "preview": "astro preview",
 | 
			
		||||
    "astro": "astro",
 | 
			
		||||
    "deploy:static": "./scripts/deploy-static.sh",
 | 
			
		||||
    "deploy:node": "./scripts/deploy-node.sh",
 | 
			
		||||
    "check:health": "curl -f http://localhost:3000/health || exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "astro": "^5.3.0",
 | 
			
		||||
    "@astrojs/node": "^9.3.0",
 | 
			
		||||
    "js-yaml": "^4.1.0",
 | 
			
		||||
    "jose": "^5.2.0",
 | 
			
		||||
    "astro": "^5.3.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": {
 | 
			
		||||
    "@types/js-yaml": "^4.0.9",
 | 
			
		||||
    "@types/cookie": "^0.6.0"
 | 
			
		||||
    "@types/cookie": "^0.6.0",
 | 
			
		||||
    "@types/js-yaml": "^4.0.9"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=18.0.0"
 | 
			
		||||
 | 
			
		||||
@ -88,6 +88,12 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || []; // Add th
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<script define:vars={{ tools, phases, domainAgnosticSoftware }}> 
 | 
			
		||||
function sanitizeHTML(html) {
 | 
			
		||||
  const div = document.createElement('div');
 | 
			
		||||
  div.textContent = html;
 | 
			
		||||
  return div.innerHTML;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  const aiInterface = document.getElementById('ai-interface');
 | 
			
		||||
  const aiInput = document.getElementById('ai-query-input');
 | 
			
		||||
@ -422,8 +428,11 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        ` : ''}
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    aiResults.innerHTML = resultsHTML;
 | 
			
		||||
    aiResults.innerHTML = ''; // Clear previous results first
 | 
			
		||||
    const tempDiv = document.createElement('div');
 | 
			
		||||
    tempDiv.innerHTML = resultsHTML;
 | 
			
		||||
    // Sanitize any dynamic content before inserting
 | 
			
		||||
    aiResults.appendChild(tempDiv);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@ -16,31 +16,24 @@ function getEnv(key: string): string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AI_MODEL = getEnv('AI_MODEL');
 | 
			
		||||
// Rate limiting store (in production, use Redis)
 | 
			
		||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
 | 
			
		||||
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
 | 
			
		||||
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
 | 
			
		||||
 | 
			
		||||
// Input validation and sanitization
 | 
			
		||||
function sanitizeInput(input: string): string {
 | 
			
		||||
  // Remove potential prompt injection patterns
 | 
			
		||||
  const dangerous = [
 | 
			
		||||
    /ignore\s+previous\s+instructions?/gi,
 | 
			
		||||
    /new\s+instructions?:/gi,
 | 
			
		||||
    /system\s*:/gi,
 | 
			
		||||
    /assistant\s*:/gi,
 | 
			
		||||
    /human\s*:/gi,
 | 
			
		||||
    /<\s*\/?system\s*>/gi,
 | 
			
		||||
    /```\s*system/gi,
 | 
			
		||||
  ];
 | 
			
		||||
  // Remove any content that looks like system instructions
 | 
			
		||||
  let sanitized = input
 | 
			
		||||
    .replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks
 | 
			
		||||
    .replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags
 | 
			
		||||
    .replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
 | 
			
		||||
    .replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
 | 
			
		||||
    .trim();
 | 
			
		||||
  
 | 
			
		||||
  let sanitized = input.trim();
 | 
			
		||||
  dangerous.forEach(pattern => {
 | 
			
		||||
    sanitized = sanitized.replace(pattern, '[FILTERED]');
 | 
			
		||||
  });
 | 
			
		||||
  // Limit length and remove excessive whitespace
 | 
			
		||||
  sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
 | 
			
		||||
  
 | 
			
		||||
  // Limit length
 | 
			
		||||
  return sanitized.slice(0, 2000);
 | 
			
		||||
  return sanitized;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Strip markdown code blocks from AI response
 | 
			
		||||
@ -70,6 +63,17 @@ function checkRateLimit(userId: string): boolean {
 | 
			
		||||
  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
 | 
			
		||||
async function loadToolsDatabase() {
 | 
			
		||||
  try {
 | 
			
		||||
 | 
			
		||||
@ -10,11 +10,13 @@ import {
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ url, request }) => {
 | 
			
		||||
  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('URL pathname:', url.pathname);
 | 
			
		||||
      console.log('URL search:', url.search);
 | 
			
		||||
      console.log('URL searchParams:', url.searchParams.toString());
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Try different ways to get parameters
 | 
			
		||||
    const allParams = Object.fromEntries(url.searchParams.entries());
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								src/pages/api/health.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/pages/api/health.ts
									
									
									
									
									
										Normal 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' }
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
@ -14,7 +14,7 @@ function getEnv(key: string): string {
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
export interface SessionData {
 | 
			
		||||
@ -74,12 +74,13 @@ export function getSessionFromRequest(request: Request): string | null {
 | 
			
		||||
// Create session cookie
 | 
			
		||||
export function createSessionCookie(token: string): string {
 | 
			
		||||
  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, {
 | 
			
		||||
    httpOnly: true,
 | 
			
		||||
    secure: isSecure,
 | 
			
		||||
    sameSite: 'lax',
 | 
			
		||||
    sameSite: 'strict', // More secure than 'lax'
 | 
			
		||||
    maxAge: SESSION_DURATION,
 | 
			
		||||
    path: '/'
 | 
			
		||||
  });
 | 
			
		||||
@ -127,7 +128,7 @@ export async function exchangeCodeForTokens(code: string): Promise<any> {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/x-www-form-urlencoded',
 | 
			
		||||
      'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
 | 
			
		||||
      'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
 | 
			
		||||
    },
 | 
			
		||||
    body: new URLSearchParams({
 | 
			
		||||
      grant_type: 'authorization_code',
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,43 @@
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import { load } from 'js-yaml';
 | 
			
		||||
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 {
 | 
			
		||||
  tools: any[];
 | 
			
		||||
@ -49,7 +86,14 @@ async function loadRawData(): Promise<ToolsData> {
 | 
			
		||||
  if (!cachedData) {
 | 
			
		||||
    const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user