본문 바로가기
코딩취미/Python

eval() 에서 악의적인 코드 실행 방지하기 : 보안 위험 방지

by 브링블링 2024. 9. 5.
반응형

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)를 활용하여 안전하고 검증된 방법으로 표현식을 평가합니다.
  • 커스텀 파서를 구현하여 필요한 기능만 제공하고, 보안성을 높입니다.
  • 사용자 입력을 직접 파싱하고 처리하여 예상치 못한 코드 실행을 방지합니다.
  • 항상 예외 처리를 포함하여 예상치 못한 에러나 공격 시도를 감지하고 적절한 대응을 합니다.
  • 보안은 다층적으로 접근해야 하며, 한 가지 방법만으로 완벽한 보안을 보장할 수 없으므로, 상황에 맞는 여러 가지 조치를 결합하여 사용합니다.
반응형