Lý do eval
và exec
rất nguy hiểm là compile
hàm mặc định sẽ tạo ra bytecode cho bất kỳ biểu thức python hợp lệ nào và mặc định eval
hoặc exec
sẽ thực thi bất kỳ bytecode của python hợp lệ nào. Tất cả các câu trả lời cho đến nay đều tập trung vào việc hạn chế mã bytecode có thể được tạo (bằng cách làm sạch đầu vào) hoặc xây dựng ngôn ngữ miền cụ thể của riêng bạn bằng AST.
Thay vào đó, bạn có thể dễ dàng tạo một eval
hàm đơn giản không có khả năng làm bất cứ điều gì bất chính và có thể dễ dàng kiểm tra thời gian chạy trên bộ nhớ hoặc thời gian được sử dụng. Tất nhiên, nếu nó là một phép toán đơn giản, thì có một phím tắt.
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]
Cách hoạt động của điều này rất đơn giản, bất kỳ biểu thức toán học hằng số nào đều được đánh giá an toàn trong quá trình biên dịch và được lưu trữ dưới dạng hằng số. Đối tượng mã được trả về bởi trình biên dịch bao gồm d
, là mã bytecode cho LOAD_CONST
, theo sau là số của hằng số cần tải (thường là đối tượng cuối cùng trong danh sách), tiếp theo S
là mã bytecode RETURN_VALUE
. Nếu phím tắt này không hoạt động, điều đó có nghĩa là đầu vào của người dùng không phải là một biểu thức hằng số (chứa một biến hoặc lệnh gọi hàm hoặc tương tự).
Điều này cũng mở ra cánh cửa cho một số định dạng đầu vào phức tạp hơn. Ví dụ:
stringExp = "1 + cos(2)"
Điều này yêu cầu thực sự đánh giá bytecode, vẫn còn khá đơn giản. Python bytecode là một ngôn ngữ hướng ngăn xếp, vì vậy mọi thứ đều là một vấn đề đơn giản TOS=stack.pop(); op(TOS); stack.put(TOS)
hoặc tương tự. Điều quan trọng là chỉ triển khai các mã opcode an toàn (tải / lưu trữ giá trị, phép toán, trả về giá trị) chứ không phải những mã không an toàn (tra cứu thuộc tính). Nếu bạn muốn người dùng có thể gọi các hàm (toàn bộ lý do không sử dụng phím tắt ở trên), hãy đơn giản thực hiện việc CALL_FUNCTION
chỉ cho phép các hàm trong danh sách 'an toàn'.
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'))
Rõ ràng, phiên bản thực của điều này sẽ dài hơn một chút (có 119 opcode, 24 trong số đó liên quan đến toán học). Việc thêm STORE_FAST
và một vài người khác sẽ cho phép đầu vào giống như 'x=5;return x+x
hoặc tương tự, dễ dàng. Nó thậm chí có thể được sử dụng để thực thi các chức năng do người dùng tạo, miễn là các chức năng do người dùng tạo tự thực thi thông qua VMeval (đừng làm cho chúng có thể gọi được !!! hoặc chúng có thể được sử dụng như một lệnh gọi lại ở đâu đó). Việc xử lý các vòng lặp yêu cầu hỗ trợ các goto
mã byte, có nghĩa là thay đổi từ một for
trình vòng lặp sang while
và duy trì một con trỏ đến lệnh hiện tại, nhưng không quá khó. Đối với khả năng chống lại DOS, vòng lặp chính nên kiểm tra thời gian đã trôi qua kể từ khi bắt đầu tính toán và một số toán tử nhất định nên từ chối đầu vào vượt quá một số giới hạn hợp lý (BINARY_POWER
là rõ ràng nhất).
Mặc dù cách tiếp cận này dài hơn một chút so với trình phân tích cú pháp ngữ pháp đơn giản cho các biểu thức đơn giản (xem ở trên về việc chỉ lấy hằng số đã biên dịch), nhưng nó mở rộng dễ dàng đến các đầu vào phức tạp hơn và không yêu cầu xử lý ngữ pháp ( compile
lấy bất cứ thứ gì phức tạp tùy ý và giảm nó thành một chuỗi các hướng dẫn đơn giản).