Belajar Desktop Developer - Network Security & WebView
Episode 18 of 28

Belajar Desktop Developer - Network Security & WebView

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

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

WebView: Zona dengan Izin Terbatas

Aturan Dasar Konfigurasi

Semua framework webview punya saklar izin yang harus dipasang ketat sejak awal:

Default aman yang wajib dimiliki
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)

Contoh Per Framework

JSElectron: BrowserWindow webPreferences ketat
new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    webSecurity: true,
    allowRunningInsecureContent: false,
  },
});
Tauri: batasi domain via capabilities
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
Tauri: capabilities dengan daftar origin remote
{
  "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.

Content Security Policy (CSP)

Menyatakan Apa yang Boleh Dimuat

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.

HTMLindex.html — CSP minimum CatatanKu
<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.

CORS dan Jaringan

CORS untuk Request dari Webview

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:

  1. Frontend request langsung — API harus mengizinkan origin aplikasi (Access-Control-Allow-Origin). Ini memaparkan token di renderer (zona tidak tepercaya).
  2. Request lewat backend — UI memanggil command IPC, backend (Rust/Node) yang melakukan HTTP request. Ini yang direkomendasikan: token API tidak pernah masuk webview, dan CORS tidak relevan karena request dari proses native.
Tauri: proxy request via backend
#[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())
}
JSElectron: fetch di main process via net
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.

JSElectron: buka link eksternal di 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);
  }
});
Tauri: delegasi link eksternal
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).

Praktik: Hardening WebView CatatanKu

Checklist yang kita terapkan:

  • Konfigurasi: sandbox aktif, node integration mati, webSecurity on, remote content dibatasi.
  • CSP: default-src 'self' + connect-src hanya domain API/update yang disetujui.
  • Jaringan: semua request lewat backend, token tidak masuk webview.
  • Link eksternal: shell.openExternal/opener untuk http(s), navigasi internal dibatasi.
  • Uji: coba muat URL remote acak di webview → harus ditolak/di-bypass ke browser.

Penutup

Inti yang harus dibawa pulang:

  • Webview adalah zona tidak tepercaya — izin dibuat eksplisit, bukan default.
  • CSP menegakkan asal muatan; jangan lumpuhkan dengan unsafe-eval.
  • CORS dihindari dengan melewatkan request melalui backend; token tidak pernah di renderer.
  • Link eksternal dibuka di browser sistem, tidak dinavigasikan di dalam webview.
  • Setiap jalur jaringan membutuhkan keputusan keamanan yang sadar — bukan bawaan framework.

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!