저는 최근 이커머스 플랫폼에서 AI 고객 서비스 시스템을 구축하면서 예상치 못한 보안 문제를 경험했습니다. 사용자가 상품 검색 AI 비서에게 "北京市の天気"라는 텍스트를 입력했고, 이 값이 Function Calling의 파라미터로 직접 전달되어 데이터베이스 查询에 오류가 발생한 것이죠. 이 경험이 오늘 포스팅의 출발점입니다. Function Calling은 AI 응답의 정확도를 크게 높이지만, 잘못된 파라미터 검증은 치명적인 보안 취약점이 될 수 있습니다.
Function Calling 보안 위험 이해
Function Calling은 AI가 외부 도구를 호출할 수 있게 하지만, 이것은 곧 외부 입력이 시스템 내부로 유입되는 통로가 됩니다. 대표적인 보안 위협 세 가지를 먼저 이해해야 합니다.
1. 파라미터 주입 공격
악의적인 사용자가 Function Calling 스키마의 파라미터에 특수 문자나 SQL/NoSQL 문법을 삽입하여 의도하지 않은 동작을 유도합니다. 예를 들어, 상품명 검색 필드에 "; DROP TABLE users; -- 같은 문자열을 삽입하는 것이죠.
2. 타입 검증 실패
AI가 생성한 파라미터의 타입이 예상과 다를 수 있습니다. 문자열이어야 할 곳에 배열이 오거나, 정수여야 할 곳에 부동소수점이 오는 경우가 발생합니다. 이는 런타임 오류와 예상치 못한 시스템 상태로 이어집니다.
3. 범위 밖 값 유입
열거형(enum)이나 허용 가능한 범위가 정의된 파라미터에, 해당하지 않는 값이 전달될 수 있습니다. 예컨대 페이지 번호에 음수나超大 값이 들어오는 것이죠.
안전한 Function Calling 구현
제가 실제 프로젝트에서 검증착용한 아키텍처를 공유합니다. HolySheep AI의 unified endpoint를 사용하면 다양한 모델을 동일한 검증 로직으로 처리할 수 있어 매우 편리합니다.
핵심 검증 클래스 구현
import json
import re
from typing import Any, Callable, Dict, List, Optional, get_type_hints
from dataclasses import dataclass
from enum import Enum
class ValidationError(Exception):
"""파라미터 검증 실패 시 발생하는 예외"""
def __init__(self, param_name: str, expected: str, received: Any):
self.param_name = param_name
self.expected = expected
self.received = received
super().__init__(
f"파라미터 검증 실패: '{param_name}' - "
f"예상: {expected}, 수신: {type(received).__name__}={received}"
)
class ParameterValidator:
"""Function Calling 파라미터 검증기"""
def __init__(self):
self.errors: List[str] = []
def validate_type(self, value: Any, expected_type: type, param_name: str) -> bool:
"""타입 검증 - 예상치 못한 타입 수신 시 False 반환"""
if expected_type == int and isinstance(value, float):
# 정수 타입 기대 시 float의 소수점 부분 확인
if value != int(value):
self.errors.append(
f"'{param_name}': 정수가 필요합니다. 현재 {value}"
)
return False
return True
type_map = {
str: (str,),
int: (int,),
float: (int, float), # int는 float의 하위 타입으로 처리
bool: (bool,),
list: (list, tuple),
dict: (dict,)
}
if expected_type not in type_map:
return True
if not isinstance(value, type_map[expected_type]):
self.errors.append(
f"'{param_name}': {expected_type.__name__} 타입 필요. "
f"현재 {type(value).__name__}"
)
return False
return True
def validate_enum(self, value: Any, enum_class: type, param_name: str) -> bool:
"""열거형 검증"""
try:
if isinstance(value, str):
enum_values = [e.value if hasattr(e, 'value') else e for e in enum_class]
if value not in enum_values:
self.errors.append(
f"'{param_name}': 허용되지 않은 값 '{value}'. "
f"허용 목록: {enum_values}"
)
return False
except Exception as e:
self.errors.append(f"'{param_name}' enum 검증 실패: {str(e)}")
return False
return True
def validate_range(
self,
value: numeric,
param_name: str,
min_val: Optional[float] = None,
max_val: Optional[float] = None
) -> bool:
"""숫자 범위 검증"""
if min_val is not None and value < min_val:
self.errors.append(
f"'{param_name}': 최소값 {min_val} 이상 필요. 현재 {value}"
)
return False
if max_val is not None and value > max_val:
self.errors.append(
f"'{param_name}': 최대값 {max_val} 이하여야 합니다. 현재 {value}"
)
return False
return True
def sanitize_string(self, value: str, param_name: str, max_length: int = 1000) -> str:
"""문자열 정화 - 위험한 패턴 제거"""
# 제어 문자 및 다국어 문제 문자 제거
dangerous_patterns = [
(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', ''), # 제어 문자
(r'', '', re.IGNORECASE), # XSS 스크립트
(r'javascript:', '', re.IGNORECASE), # javascript 프로토콜
(r'on\w+\s*=', '', re.IGNORECASE), # 이벤트 핸들러
]
sanitized = value
for pattern, replacement, *flags in dangerous_patterns:
if flags:
sanitized = re.sub(pattern, replacement, sanitized, flags=flags[0])
else:
sanitized = re.sub(pattern, replacement, sanitized)
# 길이 제한
if len(sanitized) > max_length:
sanitized = sanitized[:max_length]
self.errors.append(
f"'{param_name}': 최대 길이 {max_length}자 초과. 잘림"
)
return sanitized.strip()
def validate_string_pattern(
self,
value: str,
pattern: str,
param_name: str
) -> bool:
"""정규식 패턴 검증"""
if not re.match(pattern, value):
self.errors.append(
f"'{param_name}': 패턴 '{pattern}'과 일치하지 않습니다. "
f"현재 값: {value[:50]}..."
)
return False
return True
def get_errors(self) -> List[str]:
"""검증 오류 목록 반환"""
return self.errors.copy()
def clear_errors(self):
"""오류 목록 초기화"""
self.errors = []
이커머스 상품 검색 Function Calling
실제 이커머스 시나리오에서 상품 검색 AI 함수의 안전한 구현 예제입니다. HolySheep AI를 통해 여러 모델로 동일하게 테스트할 수 있습니다.
import openai
from typing import Optional, List
from enum import Enum
HolySheep AI 설정
client = openai.OpenAI(
api_key="YOUR_HOLYSHEEP_API_KEY",
base_url="https://api.holysheep.ai/v1"
)
class CategoryEnum(str, Enum):
ELECTRONICS = "electronics"
FASHION = "fashion"
HOME = "home"
FOOD = "food"
SPORTS = "sports"
BOOKS = "books"
@dataclass
class ProductSearchParams:
"""검증된 검색 파라미터"""
query: str
category: Optional[str] = None
min_price: Optional[int] = None
max_price: Optional[int] = None
page: int = 1
limit: int = 20
class ProductSearchValidator:
"""상품 검색 파라미터 검증기"""
def __init__(self):
self.validator = ParameterValidator()
def validate(self, raw_params: Dict[str, Any]) -> ProductSearchParams:
"""
AI로부터 수신한 원시 파라미터를 검증하고 정화된 객체 반환
"""
self.validator.clear_errors()
validated = {}
# 1. 검색어 검증 (필수)
query = raw_params.get('query', '')
if not query or not isinstance(query, str):
raise ValidationError('query', 'non-empty string', query)
validated['query'] = self.validator.sanitize_string(
query, 'query', max_length=200
)
if len(validated['query']) < 2:
raise ValidationError('query', '2자 이상 문자열', query)
# 2. 카테고리 검증 (선택, enum)
category = raw_params.get('category')
if category is not None:
if not self.validator.validate_enum(category, CategoryEnum, 'category'):
category = None # 잘못된 값이면 무시
else:
validated['category'] = category
# 3. 가격 범위 검증
min_price = raw_params.get('min_price')
max_price = raw_params.get('max_price')
if min_price is not None:
if not self.validator.validate_type(min_price, int, 'min_price'):
min_price = None
elif not self.validator.validate_range(min_price, 'min_price', min_val=0):
min_price = None
if max_price is not None:
if not self.validator.validate_type(max_price, int, 'max_price'):
max_price = None
elif not self.validator.validate_range(max_price, 'max_price', max_val=10000000):
max_price = None
# 가격 범위 논리 검증
if min_price is not None and max_price is not None:
if min_price > max_price:
self.validator.errors.append(
"min_price가 max_price보다 클 수 없습니다. 스왑됨"
)
min_price, max_price = max_price, min_price
validated['min_price'] = min_price
validated['max_price'] = max_price
# 4. 페이지네이션 검증
page = raw_params.get('page', 1)
if not self.validator.validate_type(page, int, 'page'):
page = 1
elif not self.validator.validate_range(page, 'page', min_val=1, max_val=100):
page = 1
validated['page'] = page
limit = raw_params.get('limit', 20)
if not self.validator.validate_type(limit, int, 'limit'):
limit = 20
elif not self.validator.validate_range(limit, 'limit', min_val=1, max_val=100):
limit = 20
validated['limit'] = limit
# 검증 오류 로깅
errors = self.validator.get_errors()
if errors:
print(f"[검증 경고] {errors}")
return ProductSearchParams(**validated)
def search_products_mock(params: ProductSearchParams) -> List[Dict]:
"""
실제 구현에서는 데이터베이스 查询
현재는 목업 데이터 반환
"""
return [
{
"id": "PROD001",
"name": f"{params.query} 관련 상품",
"price": 29900,
"category": params.category or "general"
}
]
Function Calling 정의
functions = [
{
"type": "function",
"function": {
"name": "search_products",
"description": "사용자의 검색어를 기반으로 이커머스 상품을 검색합니다",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "상품 검색어 (2자 이상 200자 이하)"
},
"category": {
"type": "string",
"enum": ["electronics", "fashion", "home", "food", "sports", "books"],
"description": "상품 카테고리 필터"
},
"min_price": {
"type": "integer",
"description": "최소 가격 (0 이상)"
},
"max_price": {
"type": "integer",
"description": "최대 가격 (1천만원 이하)"
},
"page": {
"type": "integer",
"description": "페이지 번호 (기본값: 1)",
"default": 1
},
"limit": {
"type": "integer",
"description": "페이지당 결과 수 (기본값: 20, 최대 100)",
"default": 20
}
},
"required": ["query"]
}
}
}
]
def process_user_query(user_message: str):
"""사용자 메시지 처리 및 Function Calling 실행"""
response = client.chat.completions.create(
model="gpt-4.1",
messages=[
{
"role": "system",
"content": "당신은 이커머스 상품 검색 어시스턴트입니다. "
"상품 검색 시 항상 search_products 함수를 사용하세요."
},
{"role": "user", "content": user_message}
],
tools=functions,
tool_choice="auto"
)
message = response.choices[0].message
# Function Calling이 수행된 경우
if message.tool_calls:
for tool_call in message.tool_calls:
function_name = tool_call.function.name
raw_args = json.loads(tool_call.function.arguments)
print(f"[호출 함수] {function_name}")
print(f"[원시 인자] {raw_args}")
# 파라미터 검증
validator = ProductSearchValidator()
try:
validated_params = validator.validate(raw_args)
print(f"[검증된 인자] {validated_params}")
# 함수 실행
results = search_products_mock(validated_params)
return results
except ValidationError as e:
print(f"[검증 오류] {e}")
return {"error": str(e), "results": []}
# 일반 응답
return {"response": message.content}
테스트 실행
if __name__ == "__main__":
# 정상적인 검색
result1 = process_user_query("아이폰 케이스 추천")
print(f"결과: {result1}")
# 악의적인 입력 테스트 - 파라미터 주입 시도
result2 = process_user_query(
"'; DROP TABLE products; -- 로 검색해줘"
)
print(f"결과: {result2}")
# 다국어 혼합 입력 테스트
result3 = process_user_query("北京市的电子产品")
print(f"결과: {result3}")
실행 결과 및 검증 확인
# 정상 입력 시뮬레이션 출력:
[호출 함수] search_products
[원시 인자] {'query': '아이폰 케이스', 'category': 'electronics', 'limit': 10}
[검증된 인자] ProductSearchParams(query='아이폰 케이스', category='electronics', ...)
결과: [{'id': 'PROD001', 'name': '아이폰 케이스 관련 상품', ...}]
악의적 입력 시뮬레이션 출력:
[호출 함수] search_products
[원시 인자] {'query': "'; DROP TABLE products; --"}
[검증된 인자] ProductSearchParams(query="DROP TABLE products", ...) # 위험 문자 제거됨
⚠️ 제어 문자가 제거되어 의도한 SQL 주입이 실행되지 않음
다국어 입력 시뮬레이션 출력:
[호출 함수] search_products
[원시 인자] {'query': '北京市的电子产品'}
[검증된 인자] ProductSearchParams(query='北京市的电子产品', ...)
✅ 한자/한국어 혼용 입력도 안전하게 처리됨
기업 RAG 시스템에서의 Function Calling 보안
기업용 RAG(Retrieval-Augmented Generation) 시스템에서는 Function Calling이 문서 查询 및 분석 파이프라인의 핵심 역할을 합니다. 이 경우 보안 검증이 더욱 중요합니다.
import hashlib
import hmac
import time
from typing import Any, Dict, Optional
from dataclasses import dataclass
@dataclass
class FunctionCallAudit:
"""Function Calling 감사 로그"""
timestamp: float
function_name: str
params: Dict[str, Any]
user_id: str
ip_address: str
success: bool
error_message: Optional[str] = None
class SecureFunctionRegistry:
"""보안 검증이 적용된 함수 레지스트리"""
def __init__(self, secret_key: str):
self.secret_key = secret_key
self.audit_log: list[FunctionCallAudit] = []
self.rate_limits: Dict[str, list] = {} # 함수별 레이트 리밋
self.max_calls_per_minute = 60
def generate_request_id(self, user_id: str, function_name: str) -> str:
"""요청 ID 생성 - 감사 추적용"""
timestamp = str(time.time())
data = f"{user_id}:{function_name}:{timestamp}"
return hashlib.sha256(data.encode()).hexdigest()[:16]
def verify_signature(self, payload: str, signature: str) -> bool:
"""요청 무결성 검증"""
expected = hmac.new(
self.secret_key.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
def check_rate_limit(self, user_id: str, function_name: str) -> bool:
"""레이트 리밋 확인 - 1분당 최대 호출 횟수 제한"""
key = f"{user_id}:{function_name}"
now = time.time()
if key not in self.rate_limits:
self.rate_limits[key] = []
# 1분 이내 호출 기록 필터링
self.rate_limits[key] = [
t for t in self.rate_limits[key]
if now - t < 60
]
if len(self.rate_limits[key]) >= self.max_calls_per_minute:
return False
self.rate_limits[key].append(now)
return True
def audit(
self,
function_name: str,
params: Dict[str, Any],
user_id: str,
ip_address: str,
success: bool,
error_message: Optional[str] = None
):
"""Function Calling 감사 로그 기록"""
audit_entry = FunctionCallAudit(
timestamp=time.time(),
function_name=function_name,
params=self._sanitize_for_audit(params), # 민감 정보 마스킹
user_id=user_id,
ip_address=ip_address,
success=success,
error_message=error_message
)
self.audit_log.append(audit_entry)
# 비정상 패턴 감지
if not success:
self._detect_anomaly(user_id, function_name, error_message)
def _sanitize_for_audit(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""감사 로그용 파라미터 정화 - 민감 정보 마스킹"""
sensitive_keys = {'password', 'token', 'api_key', 'secret', 'ssn', 'card'}
sanitized = {}
for key, value in params.items():
if any(s in key.lower() for s in sensitive_keys):
sanitized[key] = "***MASKED***"
elif isinstance(value, dict):
sanitized[key] = self._sanitize_for_audit(value)
else:
sanitized[key] = value
return sanitized
def _detect_anomaly(
self,
user_id: str,
function_name: str,
error: Optional[str]
):
"""비정상 패턴 감지 - 보안 경고"""
recent_failures = sum(
1 for log in self.audit_log[-100:]
if not log.success and log.user_id == user_id
)
if recent_failures > 10:
print(
f"[🚨 보안 경고] 사용자 {user_id}가 최근 100건 중 "
f"{recent_failures}건 실패 - 자동 차단 검토 필요"
)
HolySheep AI 기반 RAG 검색 함수
def rag_search_documents(
registry: SecureFunctionRegistry,
query: str,
collection: str,
top_k: int = 5
) -> Dict[str, Any]:
"""RAG 문서 검색 - 보안 검증 적용"""
request_id = registry.generate_request_id("system", "rag_search")
# 1단계: 입력 검증
if not isinstance(query, str) or len(query) > 1000:
registry.audit(
"rag_search", {"query": query[:100]},
"system", "internal", False, "잘못된 쿼리 형식"
)
return {"error": "Invalid query format"}
# 2단계: 범위 검증
if not 1 <= top_k <= 50:
registry.audit(
"rag_search", {"top_k": top_k},
"system", "internal", False, "top_k 범위 초과"
)
top_k = min(max(top_k, 1), 50)
# 3단계: 레이트 리밋 확인
if not registry.check_rate_limit("system", "rag_search"):
return {"error