Files
Momento/memento-note/app/actions/ollama.ts
sepehr fa72672aac
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 39s
security: fix critical auth gaps, SSRF, IDOR, and embedding error handling
CRITICAL:
- Add auth + admin check to 10 unprotected API routes (test-*, debug/*,
  config, models, fix-labels)
- Add CRON_SECRET bearer auth to /api/cron/reminders (was fully open)
- Add SSRF protection to getOllamaModels (blocks private/internal IPs)

HIGH:
- Fix getAllLabels() missing userId filter (leaked all users' labels)
- Fix /api/labels OR clause leaking other users' labels
- Fix IDOR in toggleAgent/getAgentActions (add ownership check)
- Fix getEmbeddings() returning [] on error in all 5 providers (corrupted
  semantic search with NaN cosine similarity) — now throws instead

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 21:02:13 +02:00

76 lines
2.4 KiB
TypeScript

'use server'
interface OllamaModel {
name: string
modified_at: string
size: number
digest: string
details: {
format: string
family: string
families: string[]
parameter_size: string
quantization_level: string
}
}
interface OllamaTagsResponse {
models: OllamaModel[]
}
export async function getOllamaModels(baseUrl: string): Promise<{ success: boolean; models: string[]; error?: string }> {
if (!baseUrl) {
return { success: false, models: [], error: 'Base URL is required' }
}
// Ensure URL doesn't end with slash
const cleanUrl = baseUrl.replace(/\/$/, '')
// SSRF protection: block internal/private IPs
try {
const parsed = new URL(cleanUrl)
const hostname = parsed.hostname.toLowerCase()
const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1', '169.254.169.254']
if (blockedHosts.includes(hostname)) {
return { success: false, models: [], error: 'Private/internal URLs are not allowed' }
}
if (hostname.startsWith('10.') || hostname.startsWith('172.') || hostname.startsWith('192.168.') || hostname.startsWith('fc') || hostname.startsWith('fd')) {
return { success: false, models: [], error: 'Private/internal URLs are not allowed' }
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return { success: false, models: [], error: 'Only http/https protocols allowed' }
}
} catch {
return { success: false, models: [], error: 'Invalid URL' }
}
try {
const response = await fetch(`${cleanUrl}/api/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// Set a reasonable timeout
signal: AbortSignal.timeout(5000)
})
if (!response.ok) {
throw new Error(`Ollama API returned ${response.status}: ${response.statusText}`)
}
const data = await response.json() as OllamaTagsResponse
// Extract model names
const modelNames = data.models?.map(m => m.name) || []
return { success: true, models: modelNames }
} catch (error: any) {
console.error('Failed to fetch Ollama models:', error)
return {
success: false,
models: [],
error: error.message || 'Failed to connect to Ollama'
}
}
}