Vue.js projelerindeki güvenlik riskleri ve kapatma yolları
Vue uygulamalarında sık karşılaşılan beş açık ve her biri için çözümü.
Yeni bir Vue uygulamasını yayına almak genelde küçük bir huzursuzlukla geliyor. Kullanıcı verisini açığa çıkarabilecek ya da uygulamayı tehlikeye atabilecek bir şeyi atladım mı?
Arayüze, performansa ve özelliklere saatler harcıyoruz; güvenlik çoğu zaman sıranın sonunda kalıyor. Gerekçe genelde şu: "sonuçta frontend, ne olabilir ki?" Meğer epey şey olabiliyor. Vue'nun kendisi güvenli, ama onu nasıl kullandığımız ve çevresine ne çektiğimiz açıkların çıktığı yer.
Aşağıda Vue projelerinde sık görülen beş risk ve her biri için ne yapılacağı var.
Frontend güvenliği neden önemli
Güvenliğin ağırlıklı olarak backend işi olduğunu savunabilirsin; kimlik doğrulamayı, veritabanı erişimini ve hassas her şeyi sunucu yönetiyor. Backend güvenliği kritik, doğru. Ama frontend, kullanıcının uygulamanla doğrudan temas ettiği yüzey ve ele geçirildiğinde saldırganlar oltalama ya da tuş kaydediciyle kimlik bilgisi çalabiliyor, bir mağazadaki fiyat gibi verileri değiştirebiliyor, API'n yeterince doğrulama yapmıyorsa kullanıcının hiç istemediği eylemleri tetikleyebiliyor ya da doğrudan siteyi tahrif edebiliyor.
1. Siteler arası betik çalıştırma
XSS, bir saldırganın genelde JavaScript olan bir betiği enjekte etmesi ve bu betiğin kurbanın tarayıcısında çalışması. Oradan çerezleri ve oturum jetonlarını alabiliyor ya da sayfayı yeniden yazabiliyor.
Genelde uygulama, kullanıcı girdisini temizlemeden doğrudan DOM'a bastığında oluyor. Birinin şunu gönderdiği bir yorum bölümü düşün:
<p>This is a great article!</p>
Zararsız. Şimdi buna bak:
<p>
This is a great article!
<script>
alert("You've been XSSed!");
</script>
</p>
Bileşenin bu temizlenmemiş girdiyi v-html ile bağlıyorsa, betik yorumu gören herkeste çalışıyor.
Akılda tutulacak kural: güvenilmeyen kullanıcı içeriğiyle asla v-html kullanma. Standart ara değerlemede Vue, HTML'i varsayılan olarak kaçırıyor.
<!-- Safe: Vue escapes the HTML -->
<template>
<div>
<p>{{ userComment }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const userComment = ref('<script>alert("XSS attempt!");</script>');
</script>
Bu, script etiketini çalıştırılabilir kod olarak değil düz metin olarak basıyor.
Gerçekten dinamik HTML basman gerekiyorsa, örneğin zengin metin düzenleyiciden geliyorsa, girdiyi önce backend'de DOMPurify gibi bir kütüphaneyle temizle ve saklamadan önce bunu yap; ikinci katman olarak v-html'e bir şey geçirmeden önce frontend'de de temizle.
<!-- Use v-html ONLY with trusted, sanitized content -->
<template>
<div v-html="sanitizedUserContent"></div>
</template>
<script setup>
import { ref, computed } from 'vue';
// In a real app, you'd get this from a backend API after sanitization
const rawUserContent = ref('<p>Hello <b>World</b>! <script>alert("No!");</script></p>');
// For demonstration, let's pretend this is sanitized
// In a real app, you'd use a proper library like DOMPurify
const sanitizedUserContent = computed(() => {
// This is a VERY simplistic and insecure "sanitization" for example purposes.
// DO NOT use this in production. Use a library like DOMPurify!
return rawUserContent.value.replace(/<script.*?>.*?<\/script>/g, '');
});
</script>
2. Siteler arası istek sahteciliği
CSRF, oturumu açık bir kullanıcıyı istemediği bir eylemi yapmaya kandırıyor. Saldırgan, kullanıcının tarayıcısına senin sunucuna bir istek gönderttiriyor ve kullanıcının zaten kimlik doğrulamış olmasına yaslanıyor.
Bir sekmede bankana giriş yaptığını, başka bir sekmede kötü niyetli bir site açtığını düşün. O site, bankanın para transferi uç noktasına işaret eden gizli bir form ya da bir resim etiketi barındırıyor:
<!-- Malicious website's hidden content -->
<img
src="https://yourbank.com/transfer?to=attacker&amount=1000000"
style="display:none;"
/>
Tarayıcı o resmi yüklediğinde oturum çerezlerinle birlikte bir GET isteği gönderiyor. API'nin CSRF koruması yoksa bunu meşru bir istek sayabiliyor.
Yaygın savunma CSRF jetonu. Sunucu tahmin edilemez bir jeton üretip istemciye gönderiyor: bir çerezde, bir meta etiketinde ya da ilk veri yükünde. Vue uygulamandan çıkan ve durum değiştiren her istek, yani POST, PUT ve DELETE, bu jetonu başlıklarda ya da gövdede taşıyor. Backend, jetonu eşleşmeyen her isteği reddediyor.
Pratikte HTTP istemcini jetonu kendiliğinden eklemesi için yapılandırıyorsun.
// Example using Axios (adjust based on how your backend provides the token)
import axios from "axios";
// 1. Get the CSRF token (e.g., from a meta tag in your HTML, or a cookie)
function getCsrfToken() {
// This is an example, your backend might provide it differently
const tokenElement = document.querySelector('meta[name="csrf-token"]');
return tokenElement ? tokenElement.getAttribute("content") : "";
}
const csrfToken = getCsrfToken();
// 2. Configure Axios to include the token in requests
if (csrfToken) {
axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken;
}
// Now, when you make a POST, PUT, or DELETE request, the token will be included
axios.post("/api/transfer-money", {
recipient: "myfriend",
amount: 50,
});
GET istekleri genelde jeton gerektirmiyor, çünkü idempotent olmaları bekleniyor. API'n GET üzerinde durum değiştiriyorsa bu başlı başına ayrı bir sorun.
3. Açığı olan bağımlılıklar
node_modules klasörün büyük miktarda başkasının kodu. Çoğu sorunsuz ve kapıyı aralamak için tek bir açık paket yetiyor. Ya bağımlı olduğun bir pakette, ya da onun kendi bağımlılık ağacındaki bir şeyde bilinen bir açık var; ya da bir bakımcının hesabı ele geçiriliyor ve kötü niyetli kod bir güncellemeyle geliyor.
Düzenli denetle. Hem npm audit hem yarn audit, kurulu paketleri bilinen açıklara karşı kontrol ediyor ve genelde düzeltme öneriyor.
# npm audit report
lodash <4.17.21
Severity: high
Prototype Pollution - https://npmjs.com/advisories/1526
No fix available
axios <0.21.1
Severity: moderate
Arbitrary File Write via Decompress - https://npmjs.com/advisories/1608
Fix available: `npm install axios@^0.21.1`
```
Bir bağımlılık eklemeden önce aktif bakılıp bakılmadığına, kaç kişinin kullandığına, açık güvenlik konusu olup olmadığına ve en son ne zaman güncellendiğine bak.
`package-lock.json` ya da `yarn.lock` dosyanı depoya ekle. Bu dosyalar sürümleri kesinleştiriyor; ekibin ve CI hattın aynı test edilmiş küme üzerinden derliyor.
## 4. Sırların pakete sızması
Bu kulağa apaçık geliyor ve yine de düzenli olarak oluyor. Bileşenlere ya da JavaScript dosyalarına gömülen API anahtarları istemci paketine giriyor, yani herkese açık hâle geliyor. Ayrıntılı hata mesajları backend hakkında bilgi sızdırıyor. İstemciye hiç gitmemesi gereken ortam değişkenleri pakete dahil oluyor.
Frontend kodunda asla sır gömme. Frontend'in anahtar isteyen bir API'ye ihtiyacı varsa araya kendi sunucunu koy: Vue uygulaman kendi backend'ini çağırıyor, backend de anahtarla üçüncü taraf API'sini çağırıyor. Yani tarayıcıdan `axios.get('https://api.thirdparty.com/data?apiKey=YOUR_SECRET_KEY')` yapmak yerine istek senin kontrolündeki bir sunucudan geçiyor.
Ortam değişkenleri de aynı dikkati istiyor. Vue CLI, `VUE_APP_` önekli her şeyi istemci paketine açıyor; Vite aynısını `VITE_` ile yapıyor. Oraya API uç noktalarını koy, API anahtarlarını değil. Production sırlarını sürüm kontrolünden uzak tut ve barındırma sağlayıcının yapılandırması üzerinden yönet.
Hataları da sızdırmayacak şekilde ele al. Ham backend hataları veritabanı şemasını ya da sunucu yollarını açığa çıkarabiliyor; kullanıcıya genel bir şey göster, ayrıntıyı sunucuda logla.
```html
<template>
<div>
<button @click="fetchData">Fetch Data</button>
<p v-if="error">{{ errorMessage }}</p>
<div v-if="data">{{ data }}</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import axios from "axios";
const data = ref(null);
const error = ref(false);
const errorMessage = ref("");
const fetchData = async () => {
try {
const response = await axios.get("/api/sensitive-data");
data.value = response.data;
error.value = false;
} catch (err) {
error.value = true;
// Don't show the raw error to the user!
// console.error(err.response.data); // Bad practice
errorMessage.value =
"Oops! Something went wrong. Please try again later.";
// Log the detailed error to a server-side logging service
}
};
</script>
Derlenmiş frontend'indeki her şeyin herkese açık olduğunu varsay, çünkü öyle.
5. İstemci tarafında kimlik doğrulama ve yetkilendirme
Bu Vue'ya özgü değil ama sürekli karşımıza çıkıyor. Yetkilendirmeyi yalnızca frontend'de uygulamak, ön kapıya fedai koyup arka kapıyı açık bırakmak demek.
Kendini şöyle gösteriyor: arayüzü role göre gizlemek, backend'in nasılsa kontrol edeceğini varsaymak ya da bir isAdmin bayrağını Pinia veya Vuex'te tutup onu bağlayıcı saymak.
Kimlik doğrulama gerektiren her uç nokta jetonu ya da oturumu doğrulamalı; yetki gerektiren her uç nokta da bir şey yapmadan önce o yetkiyi kontrol etmeli. Frontend kullanıcı deneyimi için var: birinin kullanamayacağı bir düğmeyi gizlemek makul, ama bu güvenlik değil ve JavaScript'ini herkes atlayabilir.
Jetonları dikkatli sakla. Backend'in koyduğu HttpOnly çerezleri jetonları istemci tarafı JavaScript'in erişemeyeceği yerde tutuyor; bu da bir XSS hatasının vereceği zararı sınırlıyor. localStorage kullanıyorsan, tamamen hiçbir yerde XSS olmamasına yaslandığını bil.
// BAD: Relying on client-side state for authorization
// In a real app, an attacker could manipulate `isAdmin` in their browser.
const user = {
name: "Alice",
isAdmin: false, // Imagine an attacker changing this to true in the console
};
if (user.isAdmin) {
// Show admin button - this is for UX, not security!
}
// GOOD: Backend always validates permissions
async function deleteUser(userId) {
try {
// Frontend sends the request
await axios.delete(`/api/users/${userId}`);
alert("User deleted!");
} catch (error) {
// Backend rejects if user is not authorized
if (error.response && error.response.status === 403) {
alert("You are not authorized to perform this action.");
} else {
alert("An error occurred.");
}
}
}
Kısaca
Vue sana güvenli bir temel veriyor, gerisi bize kalıyor. Kullanıcı girdisini temizle, özellikle v-html'e yaklaşan her şeyi. Backend'inle birlikte CSRF jetonlarını devreye al. Bağımlılıkları denetle ve güncelle. Sırları paketin dışında tut. Kimlik doğrulama ve yetkilendirmeyi her seferinde sunucuda doğrula.