Mengamankan jalur jaringan CatatanKu: konfigurasi WebView yang ketat dengan CSP, kebijakan CORS yang benar, membuka link eksternal di browser sistem, dan mengelola izin jaringan per webview

Di episode 14 kita menyatakan webview adalah zona tidak tepercaya. Episode ini membuktikan pernyataan itu secara konkret. CatatanKu kini memiliki jalur jaringan: sinkronisasi ke cloud, pemeriksaan update, dan (nanti) fitur AI. Setiap halaman yang dirender webview, setiap request yang diluncurkannya, adalah titik masuk potensial — dan karena aplikasi desktop bisa membaca file lokal, dampak kompromi webview jauh lebih besar daripada kompromi tab browser.
Episode ini mengeraskan sisi jaringan CatatanKu: konfigurasi WebView (CSP, akses jaringan per window), kebijakan CORS yang benar untuk IPC, dan penanganan link eksternal — membuka URL di browser sistem, bukan di dalam webview yang punya akses native.
Semua framework webview punya saklar izin yang harus dipasang ketat sejak awal:
JavaScript → aktif (dibutuhkan UI) tapi hanya dari konten kita
Akses file lokal → dibatasi (disable file:// dari sumber luar)
Remote content → dimuat hanya dari domain yang diizinkan
Node integration → mati total (Electron, episode 5)
Navigasi keluar → cegah; buka di browser sistem (bawah)new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
},
});use tauri::webview::WebviewWindowBuilder;
WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
.build()?;
// Domain remote yang diizinkan dikendalikan lewat
// `capabilities/default.json` → "remote" daftar origin{
"identifier": "default",
"windows": ["main"],
"remote": {
"urls": ["https://dl.catatanku.app"]
}
}Mental model yang benar: webview hanya berhak atas apa yang eksplisit diizinkan — domain tertentu untuk konten remote, command tertentu untuk IPC. Sisanya tertutup.
CSP adalah aturan yang dikirim aplikasi ke webview: dari mana script, style, gambar, dan koneksi boleh datang. Ini garis pertahanan kedua setelah konfigurasi — melindungi dari XSS meski ada konten mencurigakan yang lolos.
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.catatanku.app https://dl.catatanku.app;
"
/>Aturan yang dipakai:
default-src 'self' — basis: semua muatan dari aplikasi sendiri.script-src 'self' — script hanya dari aplikasi; tanpa unsafe-eval.connect-src — koneksi jaringan hanya ke domain API dan update yang kita izinkan.style-src 'unsafe-inline' — sering dibutuhkan framework styling; pertimbangkan menghapusnya jika memungkinkan.Di Tauri, CSP juga bisa diatur lewat tauri.conf.json; di Electron, lewat session.defaultSession.webRequest filter. Kunci yang sama: selalu tentukan CSP, jangan biarkan default longgar.
Warning
unsafe-eval dan unsafe-inline di script-src melumpuhkan manfaat CSP. Jika framework membutuhkannya, kaji ulang — banyak kasus diselesaikan dengan nonce/hash atau memindahkan logika ke backend. Setiap pengecualian CSP adalah lubang di baju zirah.
Karena UI CatatanKu adalah webview (origin tauri://localhost atau file://), request langsung ke API cloud tunduk pada aturan CORS seperti halnya browser. Ada dua jalur yang bisa dipilih:
Access-Control-Allow-Origin). Ini memaparkan token di renderer (zona tidak tepercaya).#[tauri::command]
async fn search_cloud(query: String, token: String) -> Result<String, String> {
let client = reqwest::Client::new();
let resp = client
.get("https://api.catatanku.app/search")
.header("Authorization", format!("Bearer {token}"))
.query(&[("q", query)])
.send()
.await
.map_err(|e| e.to_string())?;
resp.text().await.map_err(|e| e.to_string())
}const { net } = require("electron");
async function searchCloud(query, token) {
const request = net.request({
url: "https://api.catatanku.app/search?q=" + encodeURIComponent(query),
headers: { Authorization: `Bearer ${token}` },
});
const body = await new Promise((resolve, reject) => {
request.on("response", (response) => {
let data = "";
response.on("data", (chunk) => (data += chunk));
response.on("end", () => resolve(data));
});
request.on("error", reject);
});
request.end();
return body;
}Pola ini membunuh dua burung: token tidak terlihat webview, dan CORS tidak lagi menjadi masalah. Semua jalur jaringan di CatatanKu melewati backend — konsisten dengan arsitektur episode 2.
Saat pengguna mengklik link (https://github.com/...), apa yang terjadi? Jika webview ikut menavigasi, halaman remote itu masuk ke zona yang punya akses native — melanggar aturan "remote content hanya di domain yang diizinkan". Solusinya: tangkap navigasi keluar dan serahkan ke browser sistem.
const { shell } = require("electron");
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https://")) {
shell.openExternal(url); // browser sistem
}
return { action: "deny" }; // jangan buka jendela webview baru
});
mainWindow.webContents.on("will-navigate", (event, url) => {
if (url !== mainWindow.webContents.getURL()) {
event.preventDefault();
if (url.startsWith("https://")) shell.openExternal(url);
}
});use tauri_plugin_opener::OpenerExt;
// di frontend: window.open hanya untuk URL internal
// hook navigasi: tauri-plugin-opener menangkap http(s)://
// dan membukanya di browser sistem secara default.
app.opener().open_url(url, None::<&str>)?;Aturannya: navigasi di dalam aplikasi hanya untuk halaman internal; URL http/https apa pun dibuka di browser sistem. Kecuali domain yang memang menjadi konten aplikasi — dan domain itu pun harus didaftarkan (capabilities di Tauri).
Checklist yang kita terapkan:
default-src 'self' + connect-src hanya domain API/update yang disetujui.shell.openExternal/opener untuk http(s), navigasi internal dibatasi.Inti yang harus dibawa pulang:
unsafe-eval.Di episode 19 selanjutnya kita mengamankan data sensitif CatatanKu: secure storage & credentials — OS keychain, enkripsi, dan penyimpanan rahasia yang benar. Sampai jumpa di episode 19!