Pada aplikasi backend JavaScript atau Node.js dengan volume concurrency tinggi, pengelolaan identitas pengguna dan request context sering kali menjadi sumber celah keamanan laten. Salah satu insiden paling kritikal adalah context cross-contamination, kondisi saat state sesi milik Request A bocor dan tertukar ke Request B. Masalah ini umumnya terjadi akibat penggunaan global mutable state, singleton service yang menyimpan state sesi, atau closure scope yang tidak terisolasi dengan aman di sepanjang lifecycle event loop.

Anatomi Context Cross-Contamination pada Concurrency Tinggi

Node.js mengeksekusi operasi asynchronous berbasis single-threaded event loop. Ketika server menangani ratusan request bersamaan yang melakukan I/O non-blocking (seperti database query atau third-party API call), konteks eksekusi beralih antar-callback secara interleaving.

Pertimbangkan antipattern berikut:

// ANTIPATTERN: Shared module-level mutable state
let currentAuthUser = null;

export function setAuthUser(user) {
  currentAuthUser = user;
}

export function getAuthUser() {
  return currentAuthUser;
}

export async function handleRequest(req, res) {
  setAuthUser(req.headers['x-user-id']);
  // I/O non-blocking mengintervensi eksekusi
  await performAsyncDbQuery();
  // Di titik ini, currentAuthUser rentan ditimpa request paralel lain
  const actor = getAuthUser();
  return res.end(`Executed as ${actor}`);
}

Jika Request B mengubah currentAuthUser sebelum Request A menyelesaikan performAsyncDbQuery(), maka Request A akan mengeksekusi sisa logikanya dengan identitas Request B. Hal ini mengakibatkan session hijacking tidak disengaja, kebocoran data, atau eskalasi hak akses.

Solusi: Isolasi Menggunakan TC39 AsyncContext

Proposal TC39 AsyncContext menyediakan primitif formal di level bahasa untuk menyimpan dan meneruskan nilai secara otomatis di sepanjang alur kontrol asynchronous tanpa memerlukan prop drilling manual. Dalam ekosistem Node.js saat ini, fungsionalitas ini setara dengan modul standar node:async_hooks via AsyncLocalStorage, yang menjadi basis implementasi standar proposal tersebut.

Prinsip dasarnya: konteks diikat langsung ke rantai propagasi asynchronous (Promise/Microtask). Setiap sub-operasi asynchronous mewarisi snapshot store yang terisolasi dan tidak dapat dimodifikasi oleh rantai eksekusi lain.

Implementasi Auth Context Provider

Berikut implementasi isolasi auth context request-scoped menggunakan API standar yang kompatibel dengan proposal AsyncContext.Variable:

// auth-context.mjs
import { AsyncLocalStorage } from 'node:async_hooks';

// Menginisialisasi storage request-scoped
const authStorage = new AsyncLocalStorage();

export function runWithAuth(sessionData, fn) {
  // Membekukan objek untuk mencegah manipulasi referensi runtime
  const immutableSession = Object.freeze({ ...sessionData });
  return authStorage.run(immutableSession, fn);
}

export function getAuthContext() {
  const session = authStorage.getStore();
  if (!session) {
    throw new Error('AUTH_CONTEXT_MISSING: Operasi berada di luar execution context.');
  }
  return session;
}

Enforcing Authorization di Database Layer Tanpa Prop Drilling

Dengan context yang terisolasi, data layer dapat melakukan validasi otorisasi dan audit trail secara independen tanpa perlu menerima parameter token atau user object secara eksplisit di setiap layer fungsi.

// repository.mjs
import { getAuthContext } from './auth-context.mjs';

export async function updateResource(resourceId, data) {
  const session = getAuthContext();
  
  // Enforce tenancy dan permissions langsung di data boundary
  if (session.role !== 'admin' && session.tenantId !== data.tenantId) {
    throw new Error('FORBIDDEN: Akses cross-tenant ditolak.');
  }

  // Simulasi audit trail dan query DB
  await new Promise((resolve) => setTimeout(resolve, 50));
  return {
    updatedBy: session.userId,
    resourceId,
    status: 'success'
  };
}

Unit Test: Verifikasi Isolasi State Antar-Concurrent Request

Gunakan pengujian konkurensi native via node:test dan node:assert/strict untuk memvalidasi bahwa tidak ada context leakage saat request dieksekusi secara simultan dengan durasi latensi acak.

// auth-context.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { runWithAuth, getAuthContext } from './auth-context.mjs';
import { updateResource } from './repository.mjs';

test('Isolasi identitas sesi tetap valid di bawah beban concurrency simultan', async () => {
  const concurrentRequests = [
    { userId: 'usr_alpha', role: 'admin', tenantId: 't_1', delay: 40 },
    { userId: 'usr_beta', role: 'user', tenantId: 't_2', delay: 10 },
    { userId: 'usr_gamma', role: 'user', tenantId: 't_3', delay: 25 }
  ];

  const executeRequest = async (user) => {
    return runWithAuth(user, async () => {
      // Simulasi delay asynchronous bervariasi
      await new Promise((resolve) => setTimeout(resolve, user.delay));
      
      // Verifikasi integritas session context
      const current = getAuthContext();
      assert.equal(current.userId, user.userId);
      assert.equal(current.tenantId, user.tenantId);

      // Verifikasi eksekusi repository
      const result = await updateResource('res_123', { tenantId: user.tenantId });
      assert.equal(result.updatedBy, user.userId);
      
      return result;
    });
  };

  // Jalankan seluruh sesi secara paralel
  const results = await Promise.all(concurrentRequests.map(executeRequest));
  assert.equal(results.length, 3);
});

Trade-offs dan Mitigasi Runtime

  • Performance Overhead: Penggunaan tracing context pada Node.js internals membawa overhead mikrosekon per Promise binding. Hindari pembungkus run() berlapis-lapis di loop internal; pasang context provider hanya pada network/HTTP middleware boundary.
  • Loss of Context (Unbound Callbacks): Integrasi dengan event emitter lawas berbasis library C++ addon tertentu terkadang tidak mempropagasi execution async context secara native. Bungkus pemanggilan tersebut menggunakan AsyncLocalStorage.bind() jika kehilangan store terjadi.
  • State Mutation: Jangan simpan objek mutable di dalam context store. Selalu terapkan Object.freeze() pada payload sesi sebelum memanggil run() guna menghindari side-effects antar-komponen dalam satu request thread.