Below is a simplified model of a web-server process. The left panel shows the syscalls it legitimately needs. The right panel shows the extra syscalls an exploit would try after taking control.
Toggle calls in the allowlist on or off, then press Run exploit to see which steps succeed and which are blocked by seccomp.
<!-- Seccomp sandbox simulator: allowlist on the left, exploit steps on the right -->
<div class="container">
<div class="panel">
<div class="panel-title">Allowed syscalls (seccomp filter)</div>
<p class="hint-small">Check = allowed. Uncheck to block that syscall.</p>
<div id="allowlist" class="syscall-list"></div>
</div>
<div class="panel">
<div class="panel-title">Exploit steps</div>
<p class="hint-small">Steps the exploit tries in order. Each needs a syscall.</p>
<div id="exploit-steps" class="syscall-list readonly"></div>
</div>
</div>
<div class="controls">
<button id="run" type="button">Run exploit</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="log" class="log" aria-live="polite"></div>
/* Layout for the seccomp demo: two-panel allowlist + exploit log */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.container { display: flex; gap: 12px; margin-bottom: 10px; }
.panel { flex: 1; min-width: 0; }
.panel-title { font-weight: 700; font-size: .85rem; margin-bottom: 4px; color: #1d3557; }
.hint-small { font-size: .78rem; color: #555; margin: 0 0 6px; line-height: 1.4; }
.syscall-list { display: flex; flex-direction: column; gap: 4px; }
.syscall-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px;
border-radius: 6px; border: 1px solid #cdd9e3; background: #f0f4f8; }
.syscall-row .name { font-family: ui-monospace, monospace; font-size: .82rem;
font-weight: 600; color: #1d3557; flex: 1; }
.syscall-row .desc { font-size: .75rem; color: #555; }
.syscall-row input[type=checkbox] { cursor: pointer; accent-color: #1d3557; }
.syscall-row.readonly { background: #fff3cd; border-color: #ffc107; }
.controls { display: flex; gap: 8px; margin-bottom: 10px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { font-family: ui-monospace, monospace; font-size: .8rem; background: #1a1a2e;
color: #e0e0e0; border-radius: 8px; padding: 10px 12px; min-height: 80px;
max-height: 160px; overflow-y: auto; white-space: pre-wrap; }
.log .ok { color: #6fcf97; }
.log .bad { color: #eb5757; }
.log .info { color: #56ccf2; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Legitimate syscalls the web-server process genuinely needs — legitimate syscalls the server needs, with their descriptions
const LEGITIMATE = [
{ name: 'read', desc: 'read data from a file descriptor' },
{ name: 'write', desc: 'write data to a file descriptor' },
{ name: 'open', desc: 'open a file' },
{ name: 'close', desc: 'close a file descriptor' },
{ name: 'accept', desc: 'accept an incoming connection' },
{ name: 'send', desc: 'send data on a socket' }
];
// Syscalls an exploit would try after taking control, in order — steps an exploit might try in order, with descriptions
const EXPLOIT_STEPS = [
{ name: 'connect', desc: 'open reverse-shell connection' },
{ name: 'execve', desc: 'spawn /bin/sh' },
{ name: 'ptrace', desc: 'attach debugger to escape' },
{ name: 'mmap', desc: 'map RWX shellcode page' },
{ name: 'socket', desc: 'create new raw socket' }
];
// Render the allowlist checkboxes and exploit step rows — render the allowlist checkboxes and exploit step rows
function buildUI() {
const al = document.getElementById('allowlist');
al.innerHTML = '';
LEGITIMATE.forEach(sc => {
const row = document.createElement('div');
row.className = 'syscall-row';
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.id = 'cb-' + sc.name; cb.checked = true;
const lbl = document.createElement('label');
lbl.htmlFor = 'cb-' + sc.name; lbl.className = 'name'; lbl.textContent = sc.name;
const dsc = document.createElement('span');
dsc.className = 'desc'; dsc.textContent = sc.desc;
row.appendChild(cb); row.appendChild(lbl); row.appendChild(dsc);
al.appendChild(row);
});
const es = document.getElementById('exploit-steps');
es.innerHTML = '';
EXPLOIT_STEPS.forEach(sc => {
const row = document.createElement('div');
row.className = 'syscall-row readonly';
row.dataset.syscall = sc.name;
const nm = document.createElement('span');
nm.className = 'name'; nm.textContent = sc.name;
const dsc = document.createElement('span');
dsc.className = 'desc'; dsc.textContent = sc.desc;
row.appendChild(nm); row.appendChild(dsc);
es.appendChild(row);
});
}
// Collect the currently checked syscall names — collect the currently checked syscall names
function getAllowlist() {
return new Set(
LEGITIMATE.filter(sc => document.getElementById('cb-' + sc.name).checked)
.map(sc => sc.name)
);
}
// Simulate the exploit chain against the current allowlist — simulate the exploit chain against the current allowlist
function runExploit() {
const log = document.getElementById('log');
log.innerHTML = '';
const allowed = getAllowlist();
function line(text, cls) {
const span = document.createElement('span');
span.className = cls || '';
span.textContent = text;
log.appendChild(span);
log.appendChild(document.createTextNode('\n'));
log.scrollTop = log.scrollHeight;
}
line('[seccomp] exploit starts — testing each syscall...', 'info');
let exploitStopped = false;
for (const step of EXPLOIT_STEPS) {
if (allowed.has(step.name)) {
line(i18nFmt('[seccomp] ALLOWED %name% → exploit step succeeds', { name: step.name }), 'bad');
} else {
line(i18nFmt('[seccomp] BLOCKED %name% → SIGKILL — exploit stopped', { name: step.name }), 'ok');
exploitStopped = true;
break;
}
}
if (exploitStopped) {
line('[seccomp] Process terminated. Breach contained.', 'ok');
} else {
line('[seccomp] WARNING: all exploit steps succeeded — no containment!', 'bad');
}
}
document.getElementById('run').addEventListener('click', runExploit);
document.getElementById('reset').addEventListener('click', () => {
LEGITIMATE.forEach(sc => {
document.getElementById('cb-' + sc.name).checked = true;
});
document.getElementById('log').innerHTML = '';
});
buildUI();
<!-- Seccomp sandbox simulator: allowlist on the left, exploit steps on the right -->
<div class="container">
<div class="panel">
<div class="panel-title">Allowed syscalls (seccomp filter)</div>
<p class="hint-small">Check = allowed. Uncheck to block that syscall.</p>
<div id="allowlist" class="syscall-list"></div>
</div>
<div class="panel">
<div class="panel-title">Exploit steps</div>
<p class="hint-small">Steps the exploit tries in order. Each needs a syscall.</p>
<div id="exploit-steps" class="syscall-list readonly"></div>
</div>
</div>
<div class="controls">
<button id="run" type="button">Run exploit</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="log" class="log" aria-live="polite"></div>
/* Layout for the seccomp demo: two-panel allowlist + exploit log */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.container { display: flex; gap: 12px; margin-bottom: 10px; }
.panel { flex: 1; min-width: 0; }
.panel-title { font-weight: 700; font-size: .85rem; margin-bottom: 4px; color: #1d3557; }
.hint-small { font-size: .78rem; color: #555; margin: 0 0 6px; line-height: 1.4; }
.syscall-list { display: flex; flex-direction: column; gap: 4px; }
.syscall-row { display: flex; align-items: center; gap: 6px; padding: 5px 8px;
border-radius: 6px; border: 1px solid #cdd9e3; background: #f0f4f8; }
.syscall-row .name { font-family: ui-monospace, monospace; font-size: .82rem;
font-weight: 600; color: #1d3557; flex: 1; }
.syscall-row .desc { font-size: .75rem; color: #555; }
.syscall-row input[type=checkbox] { cursor: pointer; accent-color: #1d3557; }
.syscall-row.readonly { background: #fff3cd; border-color: #ffc107; }
.controls { display: flex; gap: 8px; margin-bottom: 10px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.log { font-family: ui-monospace, monospace; font-size: .8rem; background: #1a1a2e;
color: #e0e0e0; border-radius: 8px; padding: 10px 12px; min-height: 80px;
max-height: 160px; overflow-y: auto; white-space: pre-wrap; }
.log .ok { color: #6fcf97; }
.log .bad { color: #eb5757; }
.log .info { color: #56ccf2; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Legitimate syscalls the web-server process genuinely needs — legitimate syscalls the server needs, with their descriptions
const LEGITIMATE = [
{ name: 'read', desc: 'read data from a file descriptor' },
{ name: 'write', desc: 'write data to a file descriptor' },
{ name: 'open', desc: 'open a file' },
{ name: 'close', desc: 'close a file descriptor' },
{ name: 'accept', desc: 'accept an incoming connection' },
{ name: 'send', desc: 'send data on a socket' }
];
// Syscalls an exploit would try after taking control, in order — steps an exploit might try in order, with descriptions
const EXPLOIT_STEPS = [
{ name: 'connect', desc: 'open reverse-shell connection' },
{ name: 'execve', desc: 'spawn /bin/sh' },
{ name: 'ptrace', desc: 'attach debugger to escape' },
{ name: 'mmap', desc: 'map RWX shellcode page' },
{ name: 'socket', desc: 'create new raw socket' }
];
// Render the allowlist checkboxes and exploit step rows — render the allowlist checkboxes and exploit step rows
function buildUI() {
const al = document.getElementById('allowlist');
al.innerHTML = '';
LEGITIMATE.forEach(sc => {
const row = document.createElement('div');
row.className = 'syscall-row';
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.id = 'cb-' + sc.name; cb.checked = true;
const lbl = document.createElement('label');
lbl.htmlFor = 'cb-' + sc.name; lbl.className = 'name'; lbl.textContent = sc.name;
const dsc = document.createElement('span');
dsc.className = 'desc'; dsc.textContent = sc.desc;
row.appendChild(cb); row.appendChild(lbl); row.appendChild(dsc);
al.appendChild(row);
});
const es = document.getElementById('exploit-steps');
es.innerHTML = '';
EXPLOIT_STEPS.forEach(sc => {
const row = document.createElement('div');
row.className = 'syscall-row readonly';
row.dataset.syscall = sc.name;
const nm = document.createElement('span');
nm.className = 'name'; nm.textContent = sc.name;
const dsc = document.createElement('span');
dsc.className = 'desc'; dsc.textContent = sc.desc;
row.appendChild(nm); row.appendChild(dsc);
es.appendChild(row);
});
}
// Collect the currently checked syscall names — collect the currently checked syscall names
function getAllowlist() {
return new Set(
LEGITIMATE.filter(sc => document.getElementById('cb-' + sc.name).checked)
.map(sc => sc.name)
);
}
// Simulate the exploit chain against the current allowlist — simulate the exploit chain against the current allowlist
function runExploit() {
const log = document.getElementById('log');
log.innerHTML = '';
const allowed = getAllowlist();
function line(text, cls) {
const span = document.createElement('span');
span.className = cls || '';
span.textContent = text;
log.appendChild(span);
log.appendChild(document.createTextNode('\n'));
log.scrollTop = log.scrollHeight;
}
line('[seccomp] exploit starts — testing each syscall...', 'info');
let exploitStopped = false;
for (const step of EXPLOIT_STEPS) {
if (allowed.has(step.name)) {
line(i18nFmt('[seccomp] ALLOWED %name% → exploit step succeeds', { name: step.name }), 'bad');
} else {
line(i18nFmt('[seccomp] BLOCKED %name% → SIGKILL — exploit stopped', { name: step.name }), 'ok');
exploitStopped = true;
break;
}
}
if (exploitStopped) {
line('[seccomp] Process terminated. Breach contained.', 'ok');
} else {
line('[seccomp] WARNING: all exploit steps succeeded — no containment!', 'bad');
}
}
document.getElementById('run').addEventListener('click', runExploit);
document.getElementById('reset').addEventListener('click', () => {
LEGITIMATE.forEach(sc => {
document.getElementById('cb-' + sc.name).checked = true;
});
document.getElementById('log').innerHTML = '';
});
buildUI();
Notice that even a single blocked syscall can stop an entire attack chain. A web server that can read and write but cannot connect can never phone home. One that cannot execve can never spawn a shell. The allowlist does not need to be perfect — it just needs to remove the one call the exploit depends on.
Comments
Loading comments...