Alasan evaldan execsangat berbahaya adalah bahwa compilefungsi default akan menghasilkan bytecode untuk ekspresi python yang valid, dan default evalatau execakan 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 evalfungsi 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_FUNCTIONhanya 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_FASTdan beberapa lainnya akan memungkinkan untuk memasukkan seperti 'x=5;return x+xatau 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 gotobytecode, yang berarti mengubah dari foriterator 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 ( compileambil sesuatu yang rumit secara sewenang-wenang dan menguranginya menjadi urutan instruksi sederhana).