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_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
|
||||||
|
|
||||||
|
17
package.json
17
package.json
@ -8,23 +8,22 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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 {
|
||||||
|
@ -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('Full URL:', url.toString());
|
console.log('Auth callback processing...');
|
||||||
console.log('URL pathname:', url.pathname);
|
console.log('Full URL:', url.toString());
|
||||||
console.log('URL search:', url.search);
|
console.log('URL pathname:', url.pathname);
|
||||||
console.log('URL searchParams:', url.searchParams.toString());
|
console.log('URL search:', url.search);
|
||||||
|
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
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;
|
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',
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user