fix(voyage): permanently block cloud metadata endpoints in OTLP validator (CWE-918)
Found by simulert v4.1 smoke — doc/code-drift in v4.1 ship: docs/observability.md claims "Cloud metadata endpoints (169.254.169.254) are permanently blocked" but the validator allowed them when VOYAGE_OTEL_ALLOW_PRIVATE=1. Cloud metadata services expose IAM credentials and instance secrets — operator-trust extended to RFC-1918 home-lab access does NOT extend here, because the blast-radius (cloud-account compromise) is qualitatively different. New HARD_BLOCKED_HOSTS set checked BEFORE the link-local opt-in path: - 169.254.169.254 (AWS / GCP / Azure metadata) - 100.100.100.200 (AliCloud metadata) - metadata.google.internal - metadata.azure.com New error code ENDPOINT_HARD_BLOCKED. Existing test for ENDPOINT_LINK_LOCAL_REJECTED on 169.254.169.254 updated to assert the new code; 3 new tests verify the hard-block holds even with VOYAGE_OTEL_ALLOW_PRIVATE=1, plus AliCloud + GCP-hostname coverage. Tests: 487 → 490 pass + 2 skipped.
This commit is contained in:
parent
f4331d5d9c
commit
8dc3090080
2 changed files with 48 additions and 3 deletions
|
|
@ -11,6 +11,19 @@ import { ok, fail, issue } from '../util/result.mjs';
|
|||
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '0.0.0.0']);
|
||||
const LINK_LOCAL_PREFIXES = ['169.254.', 'fe80:'];
|
||||
|
||||
// Cloud metadata service endpoints — PERMANENTLY blocked even with
|
||||
// VOYAGE_OTEL_ALLOW_PRIVATE=1. These addresses expose IAM credentials,
|
||||
// instance secrets, and user-data on AWS/GCP/Azure/AliCloud workloads.
|
||||
// Operator-trust is NOT extended to these specific IPs because the
|
||||
// blast-radius (cloud-account compromise) is qualitatively different
|
||||
// from home-lab RFC-1918 access.
|
||||
const HARD_BLOCKED_HOSTS = new Set([
|
||||
'169.254.169.254', // AWS / GCP / Azure metadata service
|
||||
'100.100.100.200', // AliCloud metadata service
|
||||
'metadata.google.internal',
|
||||
'metadata.azure.com',
|
||||
]);
|
||||
|
||||
function isRfc1918(host) {
|
||||
// 10.0.0.0/8
|
||||
if (/^10\./.test(host)) return true;
|
||||
|
|
@ -58,7 +71,15 @@ export function validateOtlpEndpoint(url, opts = {}) {
|
|||
// Strip brackets from IPv6
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, '');
|
||||
|
||||
// Cloud metadata IP (link-local) — always rejected unless explicit opt-in
|
||||
// Cloud metadata services — PERMANENTLY blocked. VOYAGE_OTEL_ALLOW_PRIVATE
|
||||
// does NOT override this; metadata endpoints expose IAM credentials.
|
||||
if (HARD_BLOCKED_HOSTS.has(host)) {
|
||||
return fail(issue('ENDPOINT_HARD_BLOCKED',
|
||||
`Endpoint ${host} is permanently blocked (cloud metadata service). ` +
|
||||
`VOYAGE_OTEL_ALLOW_PRIVATE does not override this restriction.`));
|
||||
}
|
||||
|
||||
// Other link-local addresses — rejected unless explicit opt-in
|
||||
if (isLinkLocal(host) && !allowPrivate) {
|
||||
return fail(issue('ENDPOINT_LINK_LOCAL_REJECTED',
|
||||
`Link-local endpoint ${host} rejected (potential cloud-metadata access). ` +
|
||||
|
|
|
|||
|
|
@ -74,10 +74,34 @@ test('path-validator: FORBIDDEN_PREFIXES exports drift-pin', () => {
|
|||
|
||||
// ---- endpoint-validator: CWE-918 mitigation -------------------------------
|
||||
|
||||
test('endpoint-validator: rejects http://169.254.169.254/ link-local (cloud metadata, CWE-918)', () => {
|
||||
test('endpoint-validator: rejects http://169.254.169.254/ — PERMANENTLY blocked (CWE-918 cloud metadata)', () => {
|
||||
const r = validateOtlpEndpoint('http://169.254.169.254/v1/metrics', { env: {} });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ENDPOINT_LINK_LOCAL_REJECTED'));
|
||||
assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED'));
|
||||
});
|
||||
|
||||
test('endpoint-validator: 169.254.169.254 stays blocked EVEN WITH VOYAGE_OTEL_ALLOW_PRIVATE=1', () => {
|
||||
// Cloud metadata service is qualitatively different from RFC-1918 home-lab
|
||||
// access — operator-trust is NOT extended here. AWS/GCP/Azure metadata
|
||||
// exposes IAM credentials and can compromise the entire cloud account.
|
||||
const r = validateOtlpEndpoint('http://169.254.169.254/v1/metrics',
|
||||
{ env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } });
|
||||
assert.equal(r.valid, false, 'cloud metadata MUST stay blocked even with opt-in');
|
||||
assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED'));
|
||||
});
|
||||
|
||||
test('endpoint-validator: AliCloud metadata 100.100.100.200 PERMANENTLY blocked', () => {
|
||||
const r = validateOtlpEndpoint('http://100.100.100.200/latest/meta-data',
|
||||
{ env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED'));
|
||||
});
|
||||
|
||||
test('endpoint-validator: metadata.google.internal hostname PERMANENTLY blocked', () => {
|
||||
const r = validateOtlpEndpoint('http://metadata.google.internal/computeMetadata/v1',
|
||||
{ env: { VOYAGE_OTEL_ALLOW_PRIVATE: '1' } });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ENDPOINT_HARD_BLOCKED'));
|
||||
});
|
||||
|
||||
test('endpoint-validator: rejects http://example.com/ (requires https)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue