5분 시작하기
빈 폴더를 만들고 패키지를 설치합니다.
mkdir ssrf-guard-demo
cd ssrf-guard-demo
npm init -y
npm pkg set type=module
npm install @devslab/ssrf-guard-js
`demo.mjs` 파일을 만들고 아래 코드를 붙여넣습니다.
import { validateUrl } from '@devslab/ssrf-guard-js';
const policy = {
exactHosts: ['api.example.com'],
allowedSchemes: ['https'],
allowedPorts: [-1, 443],
};
validateUrl('https://api.example.com/v1/users', policy);
console.log('allowed');
try {
validateUrl('http://169.254.169.254/latest/meta-data/', policy);
} catch (error) {
console.log('blocked:', error.reason);
}
실행합니다.
node demo.mjs
정상 출력:
allowed
blocked: blocked_ip_literal
가장 중요한 규칙
1. 기본은 fail-closed
`exactHosts`나 `suffixes`를 설정하지 않으면 어떤 host도 허용하지 않습니다.
2. IP literal은 기본 차단
`127.0.0.1`, `2130706433`, `[::1]` 같은 우회형 IP를 URL 단계에서 막습니다.
3. redirect도 다시 검사
`safeFetch`는 302 Location이 private IP나 허용되지 않은 host로 향하면 차단합니다.
API 사용법
`validateUrl(input, policy)`
URL을 fetch하기 전에 먼저 검사합니다. 통과하면 `URL` 객체를 반환하고, 실패하면 `SsrfGuardError`를 던집니다.
import { validateUrl } from '@devslab/ssrf-guard-js';
const url = validateUrl('https://api.example.com/data', {
exactHosts: ['api.example.com'],
allowedSchemes: ['https'],
allowedPorts: [-1, 443],
});
console.log(url.href);
`safeFetch(input, policy, init?)`
URL 검증, DNS private IP 검사, redirect 재검증을 포함한 fetch helper입니다.
import { safeFetch } from '@devslab/ssrf-guard-js';
const response = await safeFetch('https://api.example.com/data', {
exactHosts: ['api.example.com'],
allowedSchemes: ['https'],
allowedPorts: [-1, 443],
});
const text = await response.text();
console.log(text);
Express / Vite / LangChain 연동
Express: middleware 한 줄 추가
사용자가 보낸 body/query 안의 URL을 검사하고, 위험한 URL이면 `400` JSON 응답을 반환합니다.
import express from 'express';
import { createExpressUrlGuard } from '@devslab/ssrf-guard-js';
const app = express();
app.use(express.json());
app.post(
'/crawl',
createExpressUrlGuard({
exactHosts: ['example.com'],
suffixes: ['example.com'],
allowedSchemes: ['https'],
}),
async (req, res) => {
res.json({ ok: true });
},
);
Vite: `vite.config.ts`에 plugin 추가
Vite dev server의 SSR/proxy endpoint가 URL을 받아 server-side fetch하는 경우에 사용합니다. 브라우저가 직접 외부로 보내는 모든 요청을 막는 도구는 아닙니다.
import { defineConfig } from 'vite';
import { ssrfGuardVitePlugin } from '@devslab/ssrf-guard-js/vite';
export default defineConfig({
plugins: [
ssrfGuardVitePlugin({
routes: ['/api/crawl'],
policy: {
suffixes: ['example.com'],
allowedSchemes: ['https'],
},
}),
],
});
아래 요청은 dev server middleware에서 차단됩니다.
/api/crawl?url=http://169.254.169.254/latest/meta-data/
LangChain / Agent Tool: tool 함수를 감싸기
모델이 tool에 넘긴 object 전체를 검사한 뒤, 안전할 때만 실제 tool 함수를 실행합니다.
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { createGuardedToolHandler, safeFetch } from '@devslab/ssrf-guard-js';
const policy = {
suffixes: ['example.com'],
allowedSchemes: ['https'],
};
export const fetchUrlTool = new DynamicStructuredTool({
name: 'fetch_url',
description: 'Fetch an allowed URL',
schema: z.object({ url: z.string().url() }),
func: createGuardedToolHandler(policy, async ({ url }) => {
const response = await safeFetch(url, policy);
return await response.text();
}),
});
LLM Tool URL 검사
LLM tool input은 top-level `url` 필드만 보면 부족합니다. 공격 URL이 nested object, array, 설명 문장 안에 숨을 수 있습니다. `guardToolInputJson`은 JSON 전체를 검사합니다.
import { guardToolInputJson } from '@devslab/ssrf-guard-js';
const toolInput = JSON.stringify({
request: {
target: 'http://169.254.169.254/latest/meta-data/',
},
});
const violation = guardToolInputJson(toolInput, {
exactHosts: ['api.example.com'],
});
if (violation) {
console.log(violation);
// 이 문자열을 tool 결과로 LLM에게 돌려주면 됩니다.
}
Policy 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
exactHosts |
[] |
정확히 일치해야 하는 host 목록입니다. 예: api.example.com |
suffixes |
[] |
example.com과 모든 하위 도메인을 허용합니다. badexample.com은 허용하지 않습니다. |
allowedSchemes |
['http', 'https'] |
보통 production에서는 ['https']로 좁히는 것을 권장합니다. |
allowedPorts |
[-1, 80, 443] |
-1은 URL에 명시 포트가 없는 경우입니다. 예: https://api.example.com/ |
rejectIpLiteralHosts |
true |
IP 주소를 host로 직접 쓰는 URL을 차단합니다. |
rejectUserInfo |
true |
https://user:pass@example.com 형태를 차단합니다. |
blockPrivateNetworks |
true |
DNS 결과가 loopback, private, link-local, metadata IP면 차단합니다. |
차단 이유
`SsrfGuardError.reason`은 아래 문자열 중 하나입니다.
blocked_scheme
blocked_host
blocked_port
blocked_ip_literal
blocked_userinfo
blocked_private_ip
blocked_redirect
blocked_other