Kebocoran data sensitif sering tidak terjadi pada saat serangan, melainkan pada saat pencatatan. Log aplikasi, alamat URL, cache, dan penyimpanan sisi klien adalah empat tempat yang jarang dihitung bagian dari permukaan kebocoran, padahal salinan data berada di sana dengan masa hidup yang panjang. Kegagalan menjaga kerahasiaan data tercatat sebagai A04:2025 Cryptographic Failures pada OWASP Top 10:2025. Topik ini memetakan keempat tempat tersebut, memperlihatkan penyamaran (masking) token pada log, dan menutup jalur kebocoran yang paling sering ditemukan pada audit. Setelah mengikuti topik ini, pembaca dapat memeriksa setiap titik penulisan data pada aplikasi dengan pertanyaan yang sama, yaitu siapa yang dapat membaca salinannya dan berapa lama salinan itu hidup.
Log tempat data berlebihan dicatat#
Log dibaca oleh lebih banyak pihak daripada yang diperkirakan saat pesannya ditulis. Operator membacanya saat insiden, sistem agregasi mengirimkannya ke platform pemantauan, dan arsip cadangan menyimpan salinannya berbulan-bulan. Konsekuensinya, setiap token atau nomor kartu yang masuk ke pesan log harus dianggap terungkap kepada semua pihak yang dapat membuka log, bukan hanya kepada aplikasinya. Kebiasaan mencatat seluruh isi permintaan saat debugging adalah penyebab yang paling sering ditemukan, karena satu baris itu membawa kredensial beserta data pelanggan sekaligus.
Penyamaran yang benar dilakukan pada satu titik sebelum catatan dibentuk, bukan diketik ulang pada setiap baris pemanggil. Modul logging Python menjalankan filter pada setiap catatan sebelum handler membentuk keluarannya, sehingga satu kelas filter cukup melindungi seluruh logger yang menggunakannya. Skrip berikut membandingkan dua logger dengan pesan yang sama: logger pertama mencatat apa adanya, sedangkan logger kedua melewati filter yang menyamarkan token dan kartu hingga hanya empat karakter terakhirnya yang tersisa.
"""Membandingkan log yang membocorkan token dengan log yang disamarkan."""
import logging
import re
import sys
POLA_KARTU = re.compile(r"\b(\d{12})(\d{4})\b")
POLA_TOKEN = re.compile(r"(?i)(token[=:\s]+)([A-Za-z0-9._-]{12,})")
def samarkan(pesan):
"""Menyembunyikan kartu dan token, hanya empat karakter terakhir yang tersisa."""
pesan = POLA_KARTU.sub(lambda m: "****-****-****-" + m.group(2), pesan)
pesan = POLA_TOKEN.sub(lambda m: m.group(1) + "****" + m.group(2)[-4:], pesan)
return pesan
class Penyamar(logging.Filter):
"""Filter logging yang menulis ulang pesan sebelum catatan dibentuk."""
def filter(self, record):
record.msg = samarkan(record.getMessage())
record.args = ()
return True
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(levelname)s %(name)s: %(message)s"))
log_cacat = logging.getLogger("layanan.cacat")
log_cacat.addHandler(handler)
log_cacat.setLevel(logging.INFO)
log_aman = logging.getLogger("layanan.aman")
log_aman.addHandler(handler)
log_aman.setLevel(logging.INFO)
log_aman.addFilter(Penyamar())
log_cacat.info("Login sukses token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9-lab")
log_cacat.info("Pembayaran kartu 4111111111111111 disetujui")
log_aman.info("Login sukses token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9-lab")
log_aman.info("Pembayaran kartu 4111111111111111 disetujui")INFO layanan.cacat: Login sukses token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9-lab
INFO layanan.cacat: Pembayaran kartu 4111111111111111 disetujui
INFO layanan.aman: Login sukses token=****-lab
INFO layanan.aman: Pembayaran kartu ****-****-****-1111 disetujuiDua baris pertama adalah kebocoran yang lengkap: token utuh memungkinkan siapa pun yang membuka log melakukan permintaan atas nama pengguna tanpa menyentuh kata sandinya. Baris ketiga dan keempat memperlihatkan tujuan penyamaran, yaitu ****-lab masih cukup untuk mengaitkan kejadian dengan sesinya, tetapi nilai yang dapat digunakan kembali sudah lenyap dari catatan. Penyamaran berpola seperti ini adalah jaring pengaman, sedangkan pertahanan pertamanya tetap berupa desain: secret yang tidak perlu diketahui aplikasi pada titik pencatatan tidak pernah dikirim ke sana.
Token pada query string#
URL adalah tempat terburuk bagi secret karena URL ditulis di banyak tempat yang tidak dikelola aplikasi. Log akses server web merekam query string apa adanya baris demi baris, dan blok berikut mengimitasi dua baris log akses untuk memperlihatkan apa yang benar-benar tersimpan di sana.
# tulis dua baris log akses imitasi, satu di antaranya membawa token
printf '%s\n' \
'10.0.0.24 - budi [07/Nov/2026:10:15:01 +0700] "GET /undang?token=rAh4sIaPpLan HTTP/1.1" 200' \
'10.0.0.24 - budi [07/Nov/2026:10:15:40 +0700] "GET /undang HTTP/1.1" 200' \
> akses.log
# tampilkan isi berkas log
cat akses.log
# bersihkan berkas contoh
rm akses.log10.0.0.24 - budi [07/Nov/2026:10:15:01 +0700] "GET /undang?token=rAh4sIaPpLan HTTP/1.1" 200
10.0.0.24 - budi [07/Nov/2026:10:15:40 +0700] "GET /undang HTTP/1.1" 200Baris pertama menyimpan token rAh4sIaPpLan secara utuh pada berkas log, dan format itulah yang ditulis oleh hampir setiap server web tanpa konfigurasi apa pun. Log akses hanyalah salah satu salinannya, karena header Referer membawa URL lengkap ke situs pihak ketiga ketika pengguna mengeklik tautan keluar, riwayat browser menyimpannya pada perangkat, dan proxy jaringan mencatatnya dalam perjalanan. Penutup jalur ini berlapis: token ditempatkan pada body POST atau header Authorization, tautan yang wajib melewati URL seperti tautan verifikasi surel dibuat sekali pakai dengan masa hidup singkat, dan header Referrer-Policy dibatasi agar URL tidak dibawa keluar bersama klik.
Cache dan penyimpanan sisi klien#
Dua salinan lain berada pada sisi klien. Salinan pertama adalah cache: respons halaman berisi data sensitif yang tidak membawa perintah cache dapat tersimpan pada cache bersama seperti proxy kantor atau CDN, lalu disajikan kepada pengguna lain yang kebetulan meminta URL yang sama. Header Cache-Control: no-store pada setiap respons terautentikasi mencegah salinannya tersimpan, baik pada cache bersama maupun cache browser.
Salinan kedua adalah penyimpanan sisi klien, dan yang paling umum adalah localStorage, yaitu penyimpanan pasangan kunci-nilai yang dirancang mudah dibaca oleh JavaScript halamannya. Konsekuensinya, token di localStorage terbaca oleh setiap skrip yang berjalan pada halaman yang sama, termasuk skrip pihak ketiga dan skrip hasil penyisipan XSS. Cookie sesi dengan atribut HttpOnly tidak terbaca JavaScript, dan perbandingan atribut cookie telah dibahas pada topik Cookie dan Session di modul Permukaan Serangan Browser.
Demo berikut mengimitasi aplikasi catatan yang menyimpan token di localStorage. Jalankan demo, klik tombol Simpan token, lalu klik Jalankan skrip penyusup, dan amati token terbaca tanpa izin apa pun.
Kode sumber demo
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Token di localStorage terbaca oleh skrip apa pun</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #f8fafc; color: #0f172a; }
h1 { font-size: 17px; margin: 0 0 4px; }
.ket { font-size: 12px; color: #475569; margin: 0 0 12px; }
.ket code { font-family: ui-monospace, monospace; background: #e2e8f0; padding: 1px 5px; border-radius: 4px; }
.baris { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
button { padding: 8px 12px; font-size: 13px; cursor: pointer; }
#simpan { background: #2563eb; border: 1px solid #1e40af; color: #fff; border-radius: 6px; }
#pencuri { background: #fee2e2; border: 1px solid #fca5a5; color: #7f1d1d; border-radius: 6px; }
#hapus { background: #fff; border: 1px solid #cbd5e1; color: #334155; border-radius: 6px; }
.kotak { border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px; font-size: 13px; margin-bottom: 8px; background: #fff; }
.kotak h2 { font-size: 13px; margin: 0 0 6px; }
.kosong { color: #94a3b8; font-style: italic; }
.token { font-family: ui-monospace, monospace; font-size: 12px; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; word-break: break-all; }
.curian { border-color: #fca5a5; background: #fef2f2; }
.curian h2 { color: #7f1d1d; }
</style>
</head>
<body>
<h1>Aplikasi catatan (imitasi)</h1>
<p class="ket">Halaman menyimpan token sesi di <code>localStorage</code>. Skrip penyusup pada demo mengimitasi skrip pihak ketiga maupun skrip hasil penyisipan yang berjalan pada halaman yang sama.</p>
<div class="baris">
<button id="simpan" type="button">Simpan token</button>
<button id="pencuri" type="button">Jalankan skrip penyusup</button>
<button id="hapus" type="button">Hapus token</button>
</div>
<div class="kotak">
<h2>Isi localStorage halaman</h2>
<div id="status" class="kosong">Token belum tersimpan.</div>
</div>
<div class="kotak curian">
<h2>Skrip penyusup berhasil membaca</h2>
<div id="curian" class="kosong">Belum ada percobaan.</div>
</div>
<script src="index.js"></script>
<script>
// Demo paparan localStorage: token yang disimpan aplikasi terbaca oleh skrip
// lain tanpa izin. Skrip penyusup di sini hanya mengimitasi perilaku skrip
// pihak ketiga atau skrip hasil penyisipan pada halaman yang sama.
(function () {
"use strict";
const KUNCI_TOKEN = "token-sesi";
const TOKEN = "tok-lab-9f83c2e1a7d64b05";
// Pratinjau tanpa akses localStorage (iframe ter-sandbox) memakai imitasi.
const memori = { nilai: null };
function bacaToken() {
try {
return localStorage.getItem(KUNCI_TOKEN);
} catch (e) {
return memori.nilai;
}
}
function simpanToken(nilai) {
try {
localStorage.setItem(KUNCI_TOKEN, nilai);
} catch (e) {
memori.nilai = nilai;
}
}
function hapusToken() {
try {
localStorage.removeItem(KUNCI_TOKEN);
} catch (e) {
memori.nilai = null;
}
}
function tampil(id, teks, kosong) {
const el = document.getElementById(id);
el.textContent = teks;
el.className = kosong ? "kosong" : "token";
}
function segarkan() {
const nilai = bacaToken();
tampil("status", nilai ? "token-sesi = " + nilai : "Token belum tersimpan.", !nilai);
}
document.getElementById("simpan").addEventListener("click", () => {
simpanToken(TOKEN);
tampil("curian", "Belum ada percobaan.", true);
segarkan();
});
document.getElementById("pencuri").addEventListener("click", () => {
const nilai = bacaToken();
tampil(
"curian",
nilai ? 'localStorage.getItem("token-sesi") → ' + nilai : "Tidak ada token yang bisa dibaca.",
!nilai
);
});
document.getElementById("hapus").addEventListener("click", () => {
hapusToken();
tampil("curian", "Belum ada percobaan.", true);
segarkan();
});
segarkan();
})();
</script>
<script>
// Demo paparan localStorage: token yang disimpan aplikasi terbaca oleh skrip
// lain tanpa izin. Skrip penyusup di sini hanya mengimitasi perilaku skrip
// pihak ketiga atau skrip hasil penyisipan pada halaman yang sama.
(function () {
"use strict";
const KUNCI_TOKEN = "token-sesi";
const TOKEN = "tok-lab-9f83c2e1a7d64b05";
// Pratinjau tanpa akses localStorage (iframe ter-sandbox) memakai imitasi.
const memori = { nilai: null };
function bacaToken() {
try {
return localStorage.getItem(KUNCI_TOKEN);
} catch (e) {
return memori.nilai;
}
}
function simpanToken(nilai) {
try {
localStorage.setItem(KUNCI_TOKEN, nilai);
} catch (e) {
memori.nilai = nilai;
}
}
function hapusToken() {
try {
localStorage.removeItem(KUNCI_TOKEN);
} catch (e) {
memori.nilai = null;
}
}
function tampil(id, teks, kosong) {
const el = document.getElementById(id);
el.textContent = teks;
el.className = kosong ? "kosong" : "token";
}
function segarkan() {
const nilai = bacaToken();
tampil("status", nilai ? "token-sesi = " + nilai : "Token belum tersimpan.", !nilai);
}
document.getElementById("simpan").addEventListener("click", () => {
simpanToken(TOKEN);
tampil("curian", "Belum ada percobaan.", true);
segarkan();
});
document.getElementById("pencuri").addEventListener("click", () => {
const nilai = bacaToken();
tampil(
"curian",
nilai ? 'localStorage.getItem("token-sesi") → ' + nilai : "Tidak ada token yang bisa dibaca.",
!nilai
);
});
document.getElementById("hapus").addEventListener("click", () => {
hapusToken();
tampil("curian", "Belum ada percobaan.", true);
segarkan();
});
segarkan();
})();
</script></body>
</html>Skrip penyusup pada demo tidak melakukan trik apa pun karena memang tidak perlu: API localStorage.getItem yang sama digunakan aplikasi dan pelaku, dan browser tidak memiliki cara membedakan keduanya. Itulah model kepercayaan halaman, bukan kelemahan salah satu API. Karena itu, token sesi diletakkan pada cookie HttpOnly, sedangkan localStorage diperuntukkan bagi data yang tidak berbahaya bila terbaca, misalnya preferensi tampilan. Apabila aplikasi satu halaman memang memerlukan token di sisi JavaScript, masa hidupnya dibuat pendek dan lingkupnya disempitkan, sehingga salinan yang terbaca cepat kehilangan nilainya.
Kesalahan umum#
Empat kekeliruan berikut sering ditemukan saat salinan data sensitif ditata ulang.
- Mengaktifkan level log debug pada produksi saat insiden. Level tersebut mencatat isi permintaan lengkap beserta header
Authorization. Naikkan level sementara itu hanya di lingkungan uji yang berisi data imitasi, bukan pada data nyata. - Menaruh token undangan dengan masa hidup panjang pada query string karena lebih mudah diuji. Salinannya berakhir pada log akses yang dibaca operator maupun mesin. Gunakan body POST saat menguji, atau gunakan opsi
-Hpadacurluntuk menyelipkan header. - Membiarkan respons terautentikasi tanpa header cache karena halamannya dianggap jarang diakses. Cache bersama tidak menilai kepopuleran URL saat menyimpan. Tambahkan
Cache-Control: no-storepada setiap respons yang mengandung data pribadi. - Menyimpan token API berumur panjang di localStorage karena memang praktis. Salinannya bertahan bahkan setelah tab ditutup. Pindahkan ke cookie
HttpOnlyatau pendekkan masa hidupnya agar nilai yang terbaca cepat kehilangan gunanya.
Ringkasan#
- Log adalah salinan data yang hidup lama dan dibaca banyak pihak, sehingga penyamaran dipasang terpusat pada filter sebelum catatan dibentuk.
- Query string tercatat pada log akses, header Referer, dan riwayat browser, sehingga token ditempatkan pada body atau header dengan masa hidup yang pendek.
Cache-Control: no-storemencegah respons terautentikasi tersimpan pada cache bersama maupun cache browser.- localStorage terbaca oleh setiap skrip halaman, sedangkan cookie
HttpOnlymenahan pembacaan JavaScript, sehingga token sesi ditempatkan pada cookie.
Referensi#
- Logging Cheat Sheet — OWASP — Panduan peristiwa yang dicatat beserta data yang tidak boleh masuk log.
- Cryptographic Storage Cheat Sheet — OWASP — Prinsip melindungi data sensitif pada saat disimpan maupun dikirim.
- A04:2025 Cryptographic Failures — OWASP Top 10 — Kategori yang menaungi kegagalan menjaga kerahasiaan data pada aplikasi web.