Alasan eval
dan exec
sangat berbahaya adalah bahwa compile
fungsi default akan menghasilkan bytecode untuk ekspresi python yang valid, dan default eval
atau exec
akan menjalankan bytecode python yang valid. Semua jawaban sampai saat ini berfokus pada pembatasan bytecode yang dapat dihasilkan (dengan membersihkan masukan) atau membangun bahasa khusus domain Anda sendiri menggunakan AST.
Sebaliknya, Anda dapat dengan mudah membuat eval
fungsi sederhana yang tidak mampu melakukan hal jahat dan dapat dengan mudah melakukan pemeriksaan waktu proses pada memori atau waktu yang digunakan. Tentunya jika matematika itu sederhana, maka ada jalan pintas.
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]
Cara kerjanya sederhana, ekspresi matematika konstan apa pun dievaluasi dengan aman selama kompilasi dan disimpan sebagai konstanta. Objek kode yang dikembalikan oleh kompilasi terdiri dari d
, yang merupakan bytecode untuk LOAD_CONST
, diikuti oleh jumlah konstanta yang akan dimuat (biasanya yang terakhir dalam daftar), diikuti oleh S
, yang merupakan bytecode untuk RETURN_VALUE
. Jika pintasan ini tidak berfungsi, berarti input pengguna bukanlah ekspresi konstan (berisi panggilan variabel atau fungsi atau serupa).
Ini juga membuka pintu ke beberapa format input yang lebih canggih. Sebagai contoh:
stringExp = "1 + cos(2)"
Ini membutuhkan evaluasi bytecode, yang masih cukup sederhana. Bytecode Python adalah bahasa berorientasi tumpukan, jadi semuanya adalah masalah sederhana TOS=stack.pop(); op(TOS); stack.put(TOS)
atau serupa. Kuncinya adalah hanya mengimplementasikan opcode yang aman (memuat / menyimpan nilai, operasi matematika, mengembalikan nilai) dan bukan yang tidak aman (pencarian atribut). Jika Anda ingin pengguna dapat memanggil fungsi (seluruh alasan untuk tidak menggunakan pintasan di atas), buat implementasi Anda CALL_FUNCTION
hanya mengizinkan fungsi dalam daftar 'aman'.
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'))
Jelas, versi sebenarnya dari ini akan sedikit lebih lama (ada 119 opcode, 24 di antaranya terkait dengan matematika). Menambahkan STORE_FAST
dan beberapa lainnya akan memungkinkan untuk memasukkan seperti 'x=5;return x+x
atau serupa, dengan mudah. Ia bahkan dapat digunakan untuk menjalankan fungsi yang dibuat pengguna, selama fungsi yang dibuat oleh pengguna dijalankan sendiri melalui VMeval (jangan membuatnya dapat dipanggil !!! atau dapat digunakan sebagai panggilan balik di suatu tempat). Menangani loop membutuhkan dukungan untuk goto
bytecode, yang berarti mengubah dari for
iterator menjadi yang paling jelas).while
dan mempertahankan pointer ke instruksi saat ini, tetapi tidak terlalu sulit. Untuk resistansi terhadap DOS, loop utama harus memeriksa berapa lama waktu telah berlalu sejak dimulainya kalkulasi, dan operator tertentu harus menolak input melebihi batas yang wajar (BINARY_POWER
Meskipun pendekatan ini agak lebih panjang dari parser tata bahasa sederhana untuk ekspresi sederhana (lihat di atas tentang hanya mengambil konstanta yang dikompilasi), pendekatan ini meluas dengan mudah ke masukan yang lebih rumit, dan tidak memerlukan penanganan tata bahasa ( compile
ambil sesuatu yang rumit secara sewenang-wenang dan menguranginya menjadi urutan instruksi sederhana).