Menjalankan bahasa R di layer backend API menggunakan framework seperti plumber atau RestRserve membawa tantangan keamanan spesifik. R dirancang secara historis untuk interaktivitas komputasi dan analisis statistik, bukan isolasi multi-tenant yang aman. Celah keamanan utama meliputi risiko Arbitrary Code Execution (ACE) akibat evaluasi dinamis, eksploitasi deserialisasi data internal, dan pembagian memori tanpa batas.
Artikel ini merinci langkah konkret untuk melakukan hardening R API service di level kode aplikasi dan infrastruktur runtime.
1. Mitigasi Arbitrary Code Execution dari Dynamic Evaluation
Pola paling berbahaya dalam API R adalah mengevaluasi string input pengguna menjadi ekspresi R menggunakan eval(parse(text = ...)). Pendekatan ini sering disalahgunakan untuk memungkinkan pengguna mengirim filter kustom atau transformasi dinamis.
# VULNERABLE: Membuka celah Remote Code Execution
# payload: {"filter": "system('id')"}
#* @post /vulnerable-query
function(req) {
raw_filter <- req$body$filter
eval(parse(text = raw_filter))
}Solusinya adalah melarang parse() pada input luar dan menggantinya dengan pendekatan deterministic whitelisting atau parsing Domain Specific Language (DSL) yang terbatas.
# HARDENED: Parameter whitelisting dan mapping aman
ALLOWED_METRICS <- c("mean", "median", "sd")
#* @post /secure-metric
#* @serializer json
function(req, res) {
metric_name <- req$body$metric
values <- as.numeric(req$body$values)
if (!metric_name %in% ALLOWED_METRICS) {
res$status <- 400
return(list(error = "Parameter metric tidak diizinkan."))
}
# Dispatch fungsi menggunakan get() dari base package namespace
metric_fn <- get(metric_name, envir = asNamespace("stats"), inherits = FALSE)
result <- metric_fn(values, na.rm = TRUE)
list(metric = metric_name, result = result)
}Jika evaluasi ekspresi matematis pengguna mutlak diperlukan, isolasi eksekusi menggunakan empty environment tanpa binding ke fungsi berbahaya seperti system, system2, atau file.remove:
# Safe evaluation environment
safe_eval <- function(expr_str, allowed_vars = list()) {
safe_env <- list2env(allowed_vars, parent = emptyenv())
# Hanya inject fungsi matematika murni
safe_env$sqrt <- base::sqrt
safe_env$log <- base::log
safe_env$exp <- base::exp
parsed <- parse(text = expr_str, keep.source = FALSE)
eval(parsed, envir = safe_env)
}2. Validasi Strict Payload JSON via Skema
R bersifat dynamic-typed dan memiliki perilaku type coercion agresif (misal: konversi string ke angka otomatis). Validasi skema sebelum payload masuk ke pipeline kalkulasi mencegah data injection dan error tak terduga.
Gunakan package jsonvalidate yang mengimplementasikan spesifikasi JSON Schema standard (Draft 7):
library(jsonvalidate)
schema <- '{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"model_id": {"type": "string", "pattern": "^[a-z0-9_-]+$"},
"batch_size": {"type": "integer", "minimum": 1, "maximum": 1000},
"features": {
"type": "array",
"items": {"type": "number"},
"minItems": 1
}
},
"required": ["model_id", "batch_size", "features"],
"additionalProperties": false
}'
v <- jsonvalidate::json_validator(schema, engine = "ajv")
# Plumber filter untuk pre-processing validasi
#* @filter validate_payload
function(req, res) {
if (req$REQUEST_METHOD %in% c("POST", "PUT", "PATCH")) {
raw_body <- req$postBody
if (length(raw_body) == 0 || !nzchar(raw_body)) {
res$status <- 400
return(list(error = "Payload kosong."))
}
is_valid <- v(raw_body, verbose = TRUE)
if (!is_valid) {
res$status <- 422
return(list(error = "Payload tidak valid.", details = attr(is_valid, "errors")))
}
}
plumber::forward()
}3. Sanitasi dan Bahaya Deserialisasi (RDS/RData)
Menerima objek biner serialisasi R (format .rds atau .RData) langsung dari HTTP request sangat tidak dianjurkan. Format serialisasi R mendukung penyertaan environment internal, referensi pointer, dan S4 objects yang dapat memicu arbitrary code saat dievaluasi atau dicetak melalui method dispatch.
Panduan penanganan payload model/data statistik:
- Gunakan format pertukaran data standar: Konversi data tabular via Arrow/Feather, Parquet, atau CSV stream. Jangan gunakan raw RDS upload di public endpoint.
- Model Serialization: Jika API melayani prediksi model machine learning, gunakan format independen seperti
ONNXatau validasi signature internal file model jika penyimpanan berada di object storage internal. - Batasi readRDS: Jangan pernah memanggil
readRDS()pada file atau stream yang diunggah oleh user untrusted tanpa checksum kriptografi (HMAC-SHA256).
4. Resource Sandboxing via Container Non-Root dan cgroups
R runtime berjalan secara single-threaded pada instance default dan mengonsumsi memori secara eager. Tanpa batas tegas, penyerang dapat memicu denial of service (OOM Kill pada host) melalui input array berukuran besar.
Konfigurasikan Docker container dengan batasan hak akses minimum dan cgroups:
# Dockerfile
FROM rocker/r-ver:4.3.2
# 1. Buat non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /home/appuser appuser
WORKDIR /app
# Copy dependency definition
COPY DESCRIPTION .
RUN R -e "install.packages(c('plumber', 'jsonvalidate'))"
COPY . /app
# 2. Limit filesystem permission
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
ENTRYPOINT ["R", "-e", "pr <- plumber::plumb('/app/entrypoint.R'); pr$run(host='0.0.0.0', port=8000)"]Terapkan resource constraints via docker-compose.yml atau Kubernetes Pod definition:
version: '3.8'
services:
r-service:
build: .
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M5. Secret Management di Runtime R
Secara default, developer R sering menggunakan file .Renviron lokal untuk menyimpan kredensial database. Pendekatan ini berisiko terekspos ke dalam version control atau tertinggal dalam image build layers.
Best Practices:
- Hindari
.Renvirondalam Image: Masukkan file tersebut ke.dockerignoredan.gitignore. - Injeksi Runtime Envs: Inject secret dari orchestrator (Kubernetes Secret / Vault) sebagai OS environment variables murni saat container start.
- Akses Aman via
Sys.getenv(): Pastikan API failure log tidak mencetak environment variable ke response error client.
# entrypoint.R
read_secret <- function(key, fallback = NULL) {
val <- Sys.getenv(key, unset = NA)
if (is.na(val)) {
if (is.null(fallback)) {
stop(sprintf("Fatal: Missing mandatory environment variable: %s", key))
}
return(fallback)
}
val
}
db_pass <- read_secret("DATABASE_PASSWORD")Peringatan Keamanan: Jangan gunakan library internal R untuk unhandled panic logging jika logger tersebut mengikutsertakan memory dump atau output dari
Sys.getenv().
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!