eval() 에서 악의적인 코드 실행 방지하기 : 보안 위험 방지
eval() 함수는 문자열로 표현된 파이썬 표현식을 실행하고 그 결과를 반환하는 강력한 함수이지만, 잘못 사용하면 심각한 보안 문제를 일으킬 수 있습니다. 특히 외부로부터 입력받은 데이터를 검증 없이 eval()에 전달하면 악의적인 코드가 실행될 수 있습니다. 이러한 보안 위험을 방어하기 위해 여러 가지 방법을 사용할 수 있습니다. 이번 포스팅에서는 eval()을 안전하게 사용하는 방법에 대해서 정리합니다.
1. 보안 위험 이해하기
먼저, eval() 함수의 보안 위험을 이해하는 것이 중요합니다. 코드에서 user_input에 악의적인 코드가 포함되어 있으며, 이를 eval()로 실행하면 시스템 전체를 삭제하는 명령어가 실행될 수 있습니다. 이러한 위험을 방어하기 위해 다양한 보안 조치를 취해야 합니다.
user_input = "import os; os.system('rm -rf /')"
eval(user_input) # 매우 위험한 코드 실행
2. eval() 사용 시 보안 강화 방법
2.1. ast.literal_eval 사용
ast.literal_eval 함수는 파이썬의 추상 구문 트리(AST)를 사용하여 문자열을 안전하게 평가합니다. 이 함수는 문자열, 숫자, 튜플, 리스트, 딕셔너리 등 리터럴 구조만을 파싱하며, 임의의 코드 실행을 방지합니다.
설명
- ast.literal_eval은 리터럴 표현식만 평가하므로, 계산식인 "2 + 3"은 평가할 수 없습니다.
- 리스트나 딕셔너리 등 데이터 구조를 안전하게 파싱할 때 유용합니다.
- 임의의 코드 실행이 불가능하므로 보안성이 높습니다.
장단점
- 장점: 매우 안전하며, 임의 코드 실행이 불가능합니다.
- 단점: 수학적 계산식이나 복잡한 표현식을 평가할 수 없습니다.
import ast
def safe_eval(user_input):
try:
result = ast.literal_eval(user_input)
return result
except (ValueError, SyntaxError):
return "Invalid input"
# 예시 사용
user_input = "2 + 3" # 이 경우에는 오류 발생
print(safe_eval(user_input)) # 출력: Invalid input
user_input = "[1, 2, 3]"
print(safe_eval(user_input)) # 출력: [1, 2, 3]
2.2. 사용자 입력 검증 및 화이트리스트 적용
입력값을 사전에 정의된 안전한 패턴이나 값으로 제한하는 방법입니다. 정규식을 사용하여 입력값이 허용된 형식인지 검증할 수 있습니다.
설명
- 정규식을 사용하여 입력값이 숫자, 공백, 연산자 및 괄호로만 구성되어 있는지 검증합니다.
- eval() 함수 호출 시 __builtins__을 None으로 설정하여 빌트인 함수들의 사용을 차단합니다.
- 안전한 수학적 계산식만 평가할 수 있도록 제한합니다.
장단점
- 장점: 허용된 패턴만 실행하므로 보안성이 높습니다.
- 단점: 허용된 패턴을 벗어나는 유효한 입력을 처리할 수 없습니다. 정규식이 복잡해질 수 있습니다.
import re
def safe_eval_expression(user_input):
# 수학적 표현식에 허용되는 문자만 포함하는지 확인
if re.match(r'^[\d\s\+\-\*\/\(\)\.]+$', user_input):
try:
return eval(user_input, {"__builtins__": None}, {})
except Exception as e:
return f"Error evaluating expression: {e}"
else:
return "Invalid input"
# 예시 사용
user_input = "2 + 3 * (4 - 1)"
print(safe_eval_expression(user_input)) # 출력: 11
user_input = "__import__('os').system('rm -rf /')"
print(safe_eval_expression(user_input)) # 출력: Invalid input
2.3. 제한된 전역 및 지역 네임스페이스 사용
eval() 함수에 전달되는 전역(globals) 및 지역(locals) 네임스페이스를 제한하여, 특정한 함수나 변수만 접근 가능하도록 설정할 수 있습니다.
설명
- globals를 {"__builtins__": None}으로 설정하여 기본 빌트인 함수들을 차단합니다.
- locals에 허용된 함수들만 포함하는 딕셔너리를 전달하여, 안전한 함수들만 사용 가능하게 합니다.
- 위험한 함수나 모듈에 대한 접근을 원천적으로 차단합니다.
장단점
- 장점: 필요한 함수만 노출하여 보안성을 높입니다.
- 단점: 허용된 함수 목록을 관리해야 하며, 필요한 모든 기능을 제공하기 어려울 수 있습니다.
def safe_eval_with_namespace(user_input):
allowed_functions = {
'abs': abs,
'round': round,
'min': min,
'max': max,
}
try:
result = eval(user_input, {"__builtins__": None}, allowed_functions)
return result
except Exception as e:
return f"Error: {e}"
# 예시 사용
user_input = "abs(-5) + max(1, 2, 3)"
print(safe_eval_with_namespace(user_input)) # 출력: 8
user_input = "__import__('os').system('ls')"
print(safe_eval_with_namespace(user_input)) # 출력: Error: name '__import__' is not defined
2.4. 커스텀 파서 및 평가기 사용
자체적으로 수식 파서를 구현하거나, 신뢰할 수 있는 서드파티 라이브러리를 사용하여 입력값을 파싱하고 계산하는 방법입니다.
설명
- sympy는 수학적 표현식을 안전하게 파싱하고 계산할 수 있는 파이썬 라이브러리입니다.
- 위험한 코드나 구문은 파싱 단계에서 차단됩니다.
- 복잡한 수학적 계산 및 심볼릭 연산도 지원합니다.
장단점
- 장점: 안전하며, 복잡한 수학적 연산을 지원합니다.
- 단점: 추가 라이브러리 설치가 필요하며, 일반적인 코드 실행에는 적합하지 않습니다.
from sympy import sympify
def safe_eval_sympy(user_input):
try:
result = sympify(user_input)
return result.evalf()
except Exception as e:
return f"Error: {e}"
# 예시 사용
user_input = "2 + 3 * (4 - 1)"
print(safe_eval_sympy(user_input)) # 출력: 11.0000000000000
user_input = "__import__('os').system('ls')"
print(safe_eval_sympy(user_input)) # 출력: Error: invalid syntax
2.5. 입력값을 파싱하여 안전하게 처리
입력값을 직접 파싱하고 필요한 작업을 수행하는 방법입니다. 이를 통해 예상치 못한 코드 실행을 방지할 수 있습니다.
설명
- 입력값을 공백으로 분리하여 숫자와 연산자를 추출합니다.
- 각 부분을 개별적으로 검증하고, 허용된 연산만 수행합니다.
- 예상치 못한 입력이나 형식에 대해서는 에러 메시지를 반환합니다.
장단점
- 장점: 완전히 제어된 환경에서 안전하게 연산을 수행할 수 있습니다.
- 단점: 복잡한 표현식이나 다양한 연산을 지원하려면 파싱 로직이 복잡해질 수 있습니다.
def safe_calculate(user_input):
try:
tokens = user_input.split()
if len(tokens) != 3:
return "Invalid input format. Use format: <number> <operator> <number>"
num1, operator, num2 = tokens
num1 = float(num1)
num2 = float(num2)
if operator == '+':
return num1 + num2
elif operator == '-':
return num1 - num2
elif operator == '*':
return num1 * num2
elif operator == '/':
return num1 / num2 if num2 != 0 else "Division by zero error"
else:
return "Unsupported operator"
except ValueError:
return "Invalid numbers provided"
# 예시 사용
user_input = "10 + 20"
print(safe_calculate(user_input)) # 출력: 30.0
user_input = "__import__('os').system('ls')"
print(safe_calculate(user_input)) # 출력: Invalid input format. Use format: <number> <operator> <number>
3. 예제: 안전한 계산기 구현
여러 가지 보안 조치를 결합하여 안전한 계산기를 구현할 수 있습니다.
설명
- AST를 직접 파싱하여 안전하게 수학적 표현식을 평가합니다.
- 지원하는 연산자를 명시적으로 정의하여, 그 외의 연산이나 함수를 사용하지 못하게 합니다.
- 예기치 않은 노드나 표현식이 들어올 경우 에러를 발생시킵니다.
- 이러한 방법은 안전성과 유연성의 균형을 맞출 수 있습니다.
장단점
- 장점: 비교적 복잡한 수학 표현식을 안전하게 평가할 수 있습니다.
- 단점: 구현이 복잡하며, 지원하지 않는 기능을 추가하려면 코드 수정이 필요합니다.
import re
import ast
import operator
# 지원하는 연산자 매핑
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg
}
def eval_expr(expr):
"""
안전한 수학 표현식 평가 함수
"""
try:
node = ast.parse(expr, mode='eval').body
return eval_node(node)
except Exception as e:
return f"Error: {e}"
def eval_node(node):
if isinstance(node, ast.Num): # 숫자 노드
return node.n
elif isinstance(node, ast.BinOp): # 이항 연산
left = eval_node(node.left)
right = eval_node(node.right)
op_type = type(node.op)
if op_type in operators:
return operators[op_type](left, right)
else:
raise ValueError(f"Unsupported operator: {op_type}")
elif isinstance(node, ast.UnaryOp): # 단항 연산
operand = eval_node(node.operand)
op_type = type(node.op)
if op_type in operators:
return operators[op_type](operand)
else:
raise ValueError(f"Unsupported operator: {op_type}")
else:
raise TypeError(f"Unsupported expression: {node}")
# 예시 사용
user_input = "2 + 3 * (4 - 1) ** 2"
print(eval_expr(user_input)) # 출력: 29
user_input = "__import__('os').system('ls')"
print(eval_expr(user_input)) # 출력: Error: Unsupported expression: <_ast.Call object at ...>
4. 정리
- 가능한 한 eval()의 사용을 피하고, 대체 가능한 안전한 방법을 사용합니다.
- **ast.literal_eval**을 사용하여 리터럴 표현식을 안전하게 평가합니다.
- 정규식 및 입력 검증을 통해 입력값을 제한하고, 허용된 패턴만 처리합니다.
- 제한된 네임스페이스를 사용하여 eval()이 접근할 수 있는 함수와 변수를 제한합니다.
- 서드파티 라이브러리(예: sympy, numexpr)를 활용하여 안전하고 검증된 방법으로 표현식을 평가합니다.
- 커스텀 파서를 구현하여 필요한 기능만 제공하고, 보안성을 높입니다.
- 사용자 입력을 직접 파싱하고 처리하여 예상치 못한 코드 실행을 방지합니다.
- 항상 예외 처리를 포함하여 예상치 못한 에러나 공격 시도를 감지하고 적절한 대응을 합니다.
- 보안은 다층적으로 접근해야 하며, 한 가지 방법만으로 완벽한 보안을 보장할 수 없으므로, 상황에 맞는 여러 가지 조치를 결합하여 사용합니다.
'코딩취미 > Python' 카테고리의 다른 글
QTreeView 트리구조 생성 및 사용방법(+이벤트 처리) (0) | 2024.09.10 |
---|---|
exec()에서 사용자 입력(악성코드) 동작 방지방 (0) | 2024.09.09 |
파이썬 리스트 객체, 원소 추가하기 : append와 extend 의 기능과 차이점 (0) | 2024.09.04 |
문자열을 파이썬 코드로 해석해서 실행하는 함수 : eval(), exec() 비교 (0) | 2024.09.04 |
파이썬 그래프 패키지 설치 방법과 특징 (Matplotlib, Seaborn, Plotly ) (0) | 2024.09.03 |