그 이유 eval와 exec그렇게 위험은 기본이다 compile기능은 유효한 파이썬 표현을위한 바이트 코드, 기본을 생성 eval하거나 exec유효한 파이썬 바이트 코드를 실행합니다. 현재까지의 모든 답변은 생성 할 수있는 바이트 코드를 제한하거나 (입력을 삭제하여) AST를 사용하여 고유 한 도메인 별 언어를 구축하는 데 중점을 두었습니다.
대신 eval악의적 인 작업을 수행 할 수없고 사용 된 메모리 또는 시간에 대한 런타임 검사를 쉽게 수행 할 수 있는 간단한 함수를 쉽게 만들 수 있습니다. 물론 간단한 수학이라면 지름길이 있습니다.
c = compile(stringExp, 'userinput', 'eval')
if c.co_code[0]==b'd' and c.co_code[3]==b'S':
return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]
이것이 작동하는 방식은 간단합니다. 상수 수학 식은 컴파일 중에 안전하게 평가되고 상수로 저장됩니다. compile에 의해 반환되는 코드 객체 d는에 대한 바이트 코드 인 LOAD_CONST,로드 할 상수 (일반적으로 목록의 마지막 항목) 번호, S에 대한 바이트 코드 인으로 구성됩니다 RETURN_VALUE. 이 단축키가 작동하지 않으면 사용자 입력이 상수 표현식이 아님을 의미합니다 (변수 또는 함수 호출 등을 포함 함).
이것은 또한 좀 더 정교한 입력 형식에 대한 문을 열어줍니다. 예를 들면 :
stringExp = "1 + cos(2)"
이를 위해서는 실제로 바이트 코드를 평가해야하며 이는 여전히 매우 간단합니다. Python 바이트 코드는 스택 지향 언어이므로 모든 것이 단순 TOS=stack.pop(); op(TOS); stack.put(TOS)하거나 유사합니다. 핵심은 안전하고 (값로드 / 저장, 수학 연산, 값 반환) 안전하지 않은 opcode (속성 조회) 만 구현하는 것입니다. 사용자가 함수를 호출 할 수 있도록하려면 (위의 바로 가기를 사용하지 않는 이유) CALL_FUNCTION'안전한'목록에있는 허용 함수 만 구현하면 됩니다.
from dis import opmap
from Queue import LifoQueue
from math import sin,cos
import operator
globs = {'sin':sin, 'cos':cos}
safe = globs.values()
stack = LifoQueue()
class BINARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get(),stack.get()))
class UNARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get()))
def CALL_FUNCTION(context, arg):
argc = arg[0]+arg[1]*256
args = [stack.get() for i in range(argc)]
func = stack.get()
if func not in safe:
raise TypeError("Function %r now allowed"%func)
stack.put(func(*args))
def LOAD_CONST(context, arg):
cons = arg[0]+arg[1]*256
stack.put(context['code'].co_consts[cons])
def LOAD_NAME(context, arg):
name_num = arg[0]+arg[1]*256
name = context['code'].co_names[name_num]
if name in context['locals']:
stack.put(context['locals'][name])
else:
stack.put(context['globals'][name])
def RETURN_VALUE(context):
return stack.get()
opfuncs = {
opmap['BINARY_ADD']: BINARY(operator.add),
opmap['UNARY_INVERT']: UNARY(operator.invert),
opmap['CALL_FUNCTION']: CALL_FUNCTION,
opmap['LOAD_CONST']: LOAD_CONST,
opmap['LOAD_NAME']: LOAD_NAME
opmap['RETURN_VALUE']: RETURN_VALUE,
}
def VMeval(c):
context = dict(locals={}, globals=globs, code=c)
bci = iter(c.co_code)
for bytecode in bci:
func = opfuncs[ord(bytecode)]
if func.func_code.co_argcount==1:
ret = func(context)
else:
args = ord(bci.next()), ord(bci.next())
ret = func(context, args)
if ret:
return ret
def evaluate(expr):
return VMeval(compile(expr, 'userinput', 'eval'))
분명히 이것의 실제 버전은 조금 더 길 것입니다 (119 개의 opcode가 있고 그 중 24 개는 수학과 관련이 있습니다). 추가 STORE_FAST및 몇 가지 다른 것은 'x=5;return x+x사소하게 쉽게 유사하거나 유사한 입력을 허용합니다 . 사용자가 만든 함수가 VMeval을 통해 자체적으로 실행되는 한 사용자가 만든 함수를 실행하는 데 사용할 수도 있습니다 (호출 가능하게 만들지 마세요 !!! 또는 어딘가에서 콜백으로 사용될 수 있음). 루프를 처리하려면 goto바이트 코드에 대한 지원이 필요합니다. 즉, for반복자 while에서 현재 명령어로의 포인터를 변경 하고 유지하는 것을 의미 하지만 너무 어렵지는 않습니다. DOS에 대한 저항을 위해 메인 루프는 계산 시작 이후 얼마나 많은 시간이 경과했는지 확인해야하며 특정 연산자는 합리적인 제한 (BINARY_POWER 가장 명백한 것).
이 접근 방식은 간단한 표현을위한 단순한 문법 파서보다 다소 길지만 (위의 컴파일 된 상수를 잡는 것에 대한 위 참조) 더 복잡한 입력으로 쉽게 확장되고 문법을 다룰 필요가 없습니다 ( compile임의로 복잡한 것을 취하고 일련의 간단한 지침).