Arsitektur yang menghubungkan browser extension sidepanel langsung ke local AI agent via loopback interface (127.0.0.1 atau localhost) sering rentan terhadap eksploitasi cross-origin. Script berbahaya di tab browsing reguler dapat mengirimkan HTTP request atau membuka koneksi WebSocket ke port daemon lokal tersebut. Tanpa mekanisme token binding di local agent API, penyerang dapat mengeksekusi instruksi arbitrary pada runtime agent pengguna.

Vektor Serangan pada Loopback Interface

Layanan lokal yang mendengarkan interface loopback sering kali mengabaikan otentikasi karena menganggap lingkungan lokal tepercaya. Hal ini membuka celah keamanan serius:

  • CORS Bypass via Simple Requests: Browser mengizinkan request cross-origin dasar (seperti HTTP POST dengan header Content-Type: text/plain) dikirimkan tanpa preflight check (OPTIONS). Meskipun browser memblokir respon agar tidak terbaca oleh tab penyerang, payload berbahaya telah terkirim dan dieksekusi oleh daemon lokal.
  • WebSocket Cross-Origin Handshake: Protokol WebSocket tidak menerapkan Same-Origin Policy (SOP). Skrip web arbitrer dari domain publik mana pun dapat menginisiasi koneksi new WebSocket('ws://127.0.0.1:port') secara langsung.
  • Token Replay Attack: Jika daemon lokal hanya menggunakan static token atau API key yang tersimpan di hardcode extension, malware lokal berkemampuan rendah atau ekstensi lain dapat membaca konfigurasi tersebut dan menggunakannya kembali tanpa batas waktu.

Arsitektur Mitigasi: Dynamic Session Binding

Untuk mengamankan interface, runtime lokal harus memverifikasi bahwa traffic benar-benar berasal dari background script extension yang sah melalui tiga lapis proteksi:

  1. Strict Origin Validation: Daemon memverifikasi header Origin secara eksklusif terhadap format internal extension ID, seperti chrome-extension://<EXTENSION_ID>. Daemon menolak nilai null, localhost, atau origin website publik.
  2. Mutual Ephemeral Token Exchange: Saat proses bootstrap, extension meminta session token dinamis dengan masa berlaku singkat (short-lived) menggunakan shared secret atau local challenge-response.
  3. Dynamic Session Binding: Server memetakan token sesi terhadap koneksi WebSocket atau ID sesi tertentu di memori. Token dikonsumsi sekali saat handshake (one-time handshake) atau dirotasi secara berkala.

Implementasi: Extension Background Script

Extension background service worker bertugas memicu handshake awal dan menyertakan token ephemeral saat menginisiasi koneksi WebSocket ke agent lokal.

// background.js (Chrome Extension Manifest V3)
const AGENT_WS_URL = 'ws://127.0.0.1:8765/v1/stream';
const AGENT_BOOTSTRAP_URL = 'http://127.0.0.1:8765/v1/auth/exchange';

async function initiateAgentSession() {
  // 1. Dapatkan dynamic handshake token via endpoint lokal
  // Browser secara otomatis menyematkan header Origin: chrome-extension://<id>
  const res = await fetch(AGENT_BOOTSTRAP_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    }
  });

  if (!res.ok) {
    throw new Error(`Gagal handshake dengan agent: ${res.statusText}`);
  }

  const { ephemeralToken } = await res.json();

  // 2. Gunakan token ephemeral pada Sec-WebSocket-Protocol atau query param terproteksi
  const ws = new WebSocket(`${AGENT_WS_URL}?token=${encodeURIComponent(ephemeralToken)}`);

  ws.onopen = () => {
    console.log('Koneksi aman ke local agent berhasil dibuat');
  };

  ws.onerror = (err) => {
    console.error('Koneksi WebSocket ditolak:', err);
  };
}

chrome.runtime.onInstalled.addListener(() => {
  initiateAgentSession();
});

Implementasi: Validasi Handshake di Runtime Agent (Go)

Di sisi daemon agent lokal (misalnya ditulis dengan Go), server memverifikasi header origin dan memberlakukan one-time ephemeral token sebelum mengizinkan upgrade koneksi HTTP ke WebSocket.

package main

import (
	"crypto/rand"
	"encoding/hex"
	"net/http"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	ExpectedExtensionID = "chrome-extension://abcdefghijklmnopabcdefghijklmnop"
	TokenTTL            = 15 * time.Second
)

type SessionStore struct {
	sync.Mutex
	tokens map[string]time.Time
}

var sessions = &SessionStore{tokens: make(map[string]time.Time)}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		origin := r.Header.Get("Origin")
		// Blokir semua origin selain ID ekstensi yang telah ditentukan
		return origin == ExpectedExtensionID
	},
}

func generateToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return hex.EncodeToString(b)
}

func exchangeHandler(w http.ResponseWriter, r *http.Request) {
	// 1. Validasi Origin wajib identik dengan extension ID
	if r.Header.Get("Origin") != ExpectedExtensionID {
		http.Error(w, "Forbidden: Invalid Origin", http.StatusForbidden)
		return
	}

	token := generateToken()
	sessions.Lock()
	sessions.tokens[token] = time.Now().Add(TokenTTL)
	sessions.Unlock()

	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(`{"ephemeralToken":"` + token + `"}`))
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	token := r.URL.Query().Get("token")

	sessions.Lock()
	expiry, exists := sessions.tokens[token]
	if exists {
		delete(sessions.tokens, token) // One-time use: hapus setelah verifikasi
	}
	sessions.Unlock()

	if !exists || time.Now().After(expiry) {
		http.Error(w, "Unauthorized: Invalid or expired token", http.StatusUnauthorized)
		return
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	defer conn.Close()

	// Handle agent logic di sini
}

func main() {
	http.HandleFunc("/v1/auth/exchange", exchangeHandler)
	http.HandleFunc("/v1/stream", wsHandler)
	http.ListenAndServe("127.0.0.1:8765", nil)
}

Trade-offs dan Pertimbangan Operasional

  • Dynamic Extension ID saat Development: Selama fase development, extension ID berubah setiap kali di-reload jika tidak menggunakan manifest key yang statis. Tambahkan properti "key" di manifest.json agar public key dan extension ID konsisten.
  • Pencegahan Port Probing: Jangan menampilkan stack trace atau banner yang mudah diidentifikasi ketika request dari origin tidak dikenal masuk. Balas request mencurigakan dengan HTTP 403 atau 404 tanpa payload verbose.
  • Binding ke UNIX Socket / Named Pipes: WebSocket loopback tetap mengekspos port ke interface jaringan lokal OS. Untuk keamanan maksimal di lingkungan multi-user desktop, gunakan Native Messaging Host sebagai jembatan transport sebelum meneruskan payload ke agent daemon.