Container Escape with Bearer Tokens
How Container Escape Manifests in Bearer Tokens
When a bearer token is over‑privileged or inadequately validated, an attacker who obtains the token can use it to call internal services that manage the container platform. In Kubernetes‑style environments, a token bearing the system:admin cluster role or similar broad permissions lets the holder list pods, exec into any container, and mount host paths. From there, the attacker can break out of the container’s isolation layer—typically by accessing /host/proc or mounting the host’s root filesystem—effectively achieving a container escape.
The vulnerable code path usually looks like a middleware or proxy that forwards the incoming Authorization: Bearer … header straight to an internal API without verifying the token’s audience, issuer, expiration, or scoped claims. If the token was issued for a different purpose (e.g., a user‑facing API) but still grants cluster‑admin rights, the proxy becomes a conduit for privilege escalation.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use('/proxy', (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).send('Missing token');
}
const token = auth.slice(7);
// ❌ No validation of audience, issuer, or expiration
const payload = jwt.decode(token); // only decode, no verify
// Use token to call internal Kubernetes API
const k8sResponse = await fetch('https://kubernetes.default.svc/api/v1/pods', {
headers: { Authorization: `Bearer ${token}` }
});
// If token has system:admin scope, attacker can list all pods, exec into privileged containers, leading to escape
res.pipe(k8sResponse.body);
});
Bearer Tokens-Specific Detection
middleBrick’s unauthenticated black‑box scan checks for the exact conditions that enable this attack vector. It inspects HTTP responses for bearer tokens that are leaked in headers, bodies, or error messages, and it validates whether any discovered token lacks essential claims.
Specifically, the scanner:
- Extracts tokens from
Authorizationheaders,Set‑Cookiefields, JSON bodies, and error payloads. - Attempts to decode JWTs (without verification) to examine
aud,iss,exp, and custom scope claims. - Flags tokens that are missing audience or issuer validation, have excessively long lifetimes (> 24 h), or contain broad privilege scopes such as
system:*,admin, orcluster:readwrite. - Checks whether the same token is accepted by internal endpoints (e.g., Kubernetes API, internal micro‑service) without additional validation, indicating a potential proxy‑style misuse.
- Reports the finding with severity, the exact location where the token was seen, and remediation guidance.
Because middleBrick requires no agents or credentials, a single URL submission yields a detailed report that highlights whether your bearer‑token handling opens a path to container escape.
Bearer Tokens-Specific Remediation
The fix is to treat every incoming bearer token as untrusted until it is cryptographically verified and its claims are explicitly checked. Use your language’s JWT library to verify the signature, then enforce audience (aud), issuer (iss), expiration (exp), and any application‑specific scopes. Short‑lived tokens (e.g., 5–15 minutes) reduce the window of abuse, and token binding to a specific client IP or TLS channel adds another layer.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const JWKS_URI = 'https://your-idp.example.com/.well-known/jwks.json';
// Simple JWKS fetch (in production cache this)
async function getSigningKey(kid) {
const res = await fetch(JWKS_URI);
const jwks = await res.json();
const key = jwks.keys.find(k => k.kid === kid);
return jwkToPem(key); // implement jwkToPem or use a library
}
app.use('/proxy', async (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).send('Missing token');
}
const token = auth.slice(7);
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded) throw new Error('Invalid token');
const signingKey = await getSigningKey(decoded.header.kid);
const payload = jwt.verify(token, signingKey, {
algorithms: ['RS256'],
audience: 'your-api-audience', // enforce aud
issuer: 'https://your-idp.example.com/', // enforce iss
});
// Application‑specific scope check
const requiredScope = 'read:orders';
if (!payload.scope || !payload.scope.includes(requiredScope)) {
return res.status(403).send('Insufficient scope');
}
// Token is now trusted – forward to internal service
const internalResp = await fetch('https://internal.api.example.com/orders', {
headers: { Authorization: `Bearer ${token}` }
});
res.status(internalResp.status).send(await internalResp.text());
} catch (err) {
console.error('Token validation failed:', err.message);
return res.status(401).send('Invalid token');
}
});
Additional hardening steps:
- Never log bearer tokens; redact them from debug output.
- Store tokens only in short‑lived, in‑memory variables; avoid persisting them to disk or localStorage.
- Use separate token audiences for external‑facing APIs versus internal platform APIs so a token issued for one cannot be used for the other.
- Rotate signing keys frequently and enforce key‑ID (
kid) validation.
By applying these controls, the bearer token can no longer be leveraged as a stepping stone to a container escape, and middleBrick will report a clean scan for this vector.