Mengekspos endpoint API yang menerima, mem-parse, dan mengeksekusi kode secara dinamis membuka celah Remote Code Execution (RCE) fatal jika tidak dirancang dengan benar. Evaluasi ekspresi menggunakan eval() atau exec() langsung terhadap input pengguna memberikan akses penuh ke runtime host, manipulasi memori internal via __subclasses__(), hingga eksfiltrasi filesystem.
Untuk mengamankan antarmuka interpreter—seperti yang diimplementasikan pada toy interpreter atau domain-specific language (DSL)—kita wajib menerapkan inspeksi statis lewat modul ast (Abstract Syntax Tree) sebelum eksekusi dimulai, didukung dengan pembatasan resource level sistem operasi.
Anatomi Risiko RCE pada Python Interpreter Publik
Penyerang jarang memanggil import os; os.system('sh') secara terang-terangan jika ada filter regex sederhana. Sebaliknya, teknik Python sandbox escape memanfaatkan introspeksi objek runtime Python:
# Membuka shell via introspeksi kelas tanpa memanggil kata 'import'
().__class__.__base__.__subclasses__()[137].__init__.__globals__['system']('whoami')Filter berbasis string atau blacklist regex tidak memadai karena Python mendukung decoding hex, unicode bypass, dan dynamic attribute lookup via getattr(). Sanitasi wajib dilakukan pada level pohon sintaks (AST) dengan skema default-deny whitelist.
1. Validasi Ukuran Payload dan Sanitasi Token
Tahap pertama adalah mencegah Denial of Service (DoS) pada parser AST itu sendiri. Payload raksasa atau rekursi kurung buka tak terbatas dapat menyebabkan RecursionError atau memory exhaustion saat ast.parse() bekerja.
MAX_CODE_LENGTH = 4096 # 4 KB
def validate_payload(code: str) -> None:
if not code or not code.strip():
raise ValueError("Payload kode tidak boleh kosong.")
if len(code.encode('utf-8')) > MAX_CODE_LENGTH:
raise ValueError(f"Ukuran kode melampaui limit {MAX_CODE_LENGTH} bytes.")2. Pembatasan Node Menggunakan ast.NodeVisitor
Modul ast bawaan Python mengurai kode menjadi token hierarkis tanpa mengeksekusinya. Subclassing ast.NodeVisitor memungkinkan kita memeriksa setiap node. Jika node tidak berada di daftar izin eksplisit, proses langsung digagalkan.
import ast
class SecurityViolation(Exception):
pass
class SafeASTValidator(ast.NodeVisitor):
# Whitelist node dasar untuk DSL aritmatika dan kontrol alur sederhana
ALLOWED_NODES = {
ast.Module, ast.Expr, ast.Assign, ast.Constant,
ast.Name, ast.BinOp, ast.UnaryOp, ast.operator,
ast.unaryop, ast.Add, ast.Sub, ast.Mult, ast.Div,
ast.Compare, ast.cmpop, ast.Eq, ast.NotEq, ast.Lt, ast.Gt
}
# Nama atribut dan variabel terlarang untuk mencegah introspeksi
BANNED_NAMES = {
'__class__', '__base__', '__bases__', '__subclasses__',
'__globals__', '__code__', '__closure__', '__import__',
'open', 'eval', 'exec', 'compile', 'getattr', 'setattr'
}
def generic_visit(self, node):
if type(node) not in self.ALLOWED_NODES:
raise SecurityViolation(f"Node terlarang terdeteksi: {type(node).__name__}")
super().generic_visit(node)
def visit_Name(self, node):
if node.id in self.BANNED_NAMES or node.id.startswith('__'):
raise SecurityViolation(f"Akses identifier terlarang: {node.id}")
self.generic_visit(node)
def visit_Attribute(self, node):
# Blokir total akses atribut objek (mencegah instance.method atau class traversal)
raise SecurityViolation(f"Akses atribut dot (.) tidak diizinkan: {node.attr}")
def visit_Call(self, node):
# Blokir eksekusi fungsi langsung jika interpreter tidak membutuhkannya
raise SecurityViolation("Pemanggilan fungsi dilarang pada sandbox ini.")Penggunaan visit_Attribute dengan penolakan langsung menutup celah traversal dunder methods (seperti __subclasses__) yang menjadi vektor utama sandbox escape.
3. Isolasi Resource Runtime (CPU Time dan Memory Limit)
Meskipun AST lolos sanitasi, kode valid seperti 9 ** 99999999 atau while True: pass dapat membekukan worker thread/process. Kita menggunakan modul resource level kernel Linux untuk membatasi CPU time dan virtual memory (RLIMIT).
import resource
import signal
def set_process_limits(max_cpu_seconds: int = 2, max_memory_mb: int = 64):
# Batasi waktu eksekusi CPU
resource.setrlimit(resource.RLIMIT_CPU, (max_cpu_seconds, max_cpu_seconds))
# Batasi memory heap (virtual memory)
max_bytes = max_memory_mb * 1024 * 1024
resource.setrlimit(resource.RLIMIT_AS, (max_bytes, max_bytes))Catatan: Eksekusi wajib dijalankan dalam child process via
multiprocessing.Processatau worker terpisah agar batasan resource tidak menghentikan master process API.
4. Implementasi API Runner dan Kontrak Respons Deterministik
API sandbox harus mengembalikan error yang terstruktur dan deterministik tanpa membocorkan internal stack trace server. Parsing error dibagi menjadi tiga kategori: SYNTAX_ERROR, SECURITY_VIOLATION, dan RUNTIME_ERROR.
import sys
import ast
from typing import Tuple, Dict, Any
def execute_sandbox(source_code: str) -> Tuple[bool, Dict[str, Any]]:
# 1. Validasi payload awal
try:
validate_payload(source_code)
tree = ast.parse(source_code)
except SyntaxError as e:
return False, {"error_type": "SYNTAX_ERROR", "message": str(e), "line": e.lineno}
except ValueError as e:
return False, {"error_type": "PAYLOAD_INVALID", "message": str(e)}
# 2. Validasi AST Whitelist
validator = SafeASTValidator()
try:
validator.visit(tree)
except SecurityViolation as e:
return False, {"error_type": "SECURITY_VIOLATION", "message": str(e)}
# 3. Eksekusi terbatas dalam namespace kosong
restricted_scope = {"__builtins__": {}}
try:
# ponytail: implementasikan timeout thread/subprocessing pada level worker
compiled_code = compile(tree, filename="<sandbox>", mode="exec")
exec(compiled_code, restricted_scope, restricted_scope)
# Hapus kunci builtins dari output
restricted_scope.pop("__builtins__", None)
return True, {"status": "SUCCESS", "variables": restricted_scope}
except Exception as e:
return False, {"error_type": "RUNTIME_ERROR", "message": f"{type(e).__name__}: {str(e)}"}Format Respons JSON Deterministik
Saat terjadi pelanggaran keamanan, respons API memberikan status kode 422 Unprocessable Content atau 400 Bad Request dengan struktur yang konsisten:
{
"success": false,
"error": {
"type": "SECURITY_VIOLATION",
"message": "Node terlarang terdeteksi: Import",
"code": 4001
}
}Trade-offs dan Lapisan Pertahanan Tambahan
Membatasi AST saja di dalam interpreter Python tidak pernah cukup untuk kode untrusted berskala produksi. Python runtime didesain fleksibel, bukan terisolasi secara kriptografis.
- Trade-off Fleksibilitas: Semakin ketat AST whitelist, semakin terbatas fungsionalitas bahasa yang dapat digunakan. Jika butuh eksekusi Python penuh (termasuk modul eksternal), sanitasi AST tidak cocok.
- Defense in Depth: Selalu kombinasikan sanitasi AST dengan isolasi container ephemeral (misalnya Docker dengan
--read-onlyrootfs,--network=none, dan user non-root) atau microVM seperti Firecracker dan gVisor untuk memastikan kernel host tetap aman jika terjadi zero-day breakout.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!