Skip to content

Commit 4c6f319

Browse files
Potential fix for code scanning alert no. 12: Server-side request forgery
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 82ed172 commit 4c6f319

1 file changed

Lines changed: 52 additions & 2 deletions

File tree

app/api/admin/integrations/test/route.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,52 @@ interface TestIntegrationBody {
1313
credentials: Record<string, string>
1414
}
1515

16+
function isPrivateOrLocalHost(hostname: string): boolean {
17+
const host = hostname.toLowerCase()
18+
19+
if (host === 'localhost' || host === '::1' || host === '[::1]') return true
20+
if (host.endsWith('.localhost') || host.endsWith('.local')) return true
21+
22+
// IPv4 checks
23+
const ipv4Match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
24+
if (ipv4Match) {
25+
const octets = ipv4Match.slice(1).map((n) => Number(n))
26+
if (octets.some((o) => Number.isNaN(o) || o < 0 || o > 255)) return true
27+
28+
const [a, b] = octets
29+
if (a === 10) return true
30+
if (a === 127) return true
31+
if (a === 169 && b === 254) return true
32+
if (a === 172 && b >= 16 && b <= 31) return true
33+
if (a === 192 && b === 168) return true
34+
if (a === 0) return true
35+
}
36+
37+
// Common IPv6 local/private forms
38+
const normalized = host.replace(/^\[|\]$/g, '')
39+
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true // ULA
40+
if (normalized.startsWith('fe80:')) return true // link-local
41+
42+
return false
43+
}
44+
45+
function getSafeKenerOrigin(baseUrl?: string): string | null {
46+
const candidate = (baseUrl && baseUrl.trim()) || 'https://emberlystat.us'
47+
48+
let parsed: URL
49+
try {
50+
parsed = new URL(candidate)
51+
} catch {
52+
return null
53+
}
54+
55+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null
56+
if (parsed.username || parsed.password) return null
57+
if (isPrivateOrLocalHost(parsed.hostname)) return null
58+
59+
return parsed.origin
60+
}
61+
1662
interface TestResult {
1763
ok: boolean
1864
message: string
@@ -122,9 +168,13 @@ async function testGitHub(pat: string, org?: string): Promise<TestResult> {
122168
}
123169

124170
async function testKener(apiKey: string, baseUrl?: string): Promise<TestResult> {
125-
const url = (baseUrl || 'https://emberlystat.us').replace(/\/$/, '')
171+
const origin = getSafeKenerOrigin(baseUrl)
172+
if (!origin) {
173+
return { ok: false, message: 'Invalid Kener base URL' }
174+
}
175+
126176
try {
127-
const res = await fetch(`${url}/api/v4/monitors`, {
177+
const res = await fetch(`${origin}/api/v4/monitors`, {
128178
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
129179
signal: AbortSignal.timeout(8000),
130180
})

0 commit comments

Comments
 (0)