mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
64136484ee
Remediates the findings from the adversarial audit (threat model: malicious plugin author + malicious artifact). Highlights: Critical - proxy: force nosniff + Content-Disposition: attachment on every proxied reply and drop location/content-disposition + non-2xx from the passthrough, so a plugin can't serve an HTML document at TREK's origin (sandbox-escape → account takeover) or an open redirect. High - db:own runs synchronously in the host: cap the plugin DB (max_page_count) and row-cap query() via iterate() so a recursive CTE / huge blob can't stall the event loop, OOM, or exhaust the shared volume. - supervisor: measure child RSS host-side (/proc/<pid>/statm) instead of trusting the spoofable heartbeat; add an activation timeout so a stuck onLoad can't hang activate() or peg a core unreaped. - safe-extract: enforce entry-count + cumulative-size limits INSIDE readZip before inflating (decompression-bomb OOM). - re-consent: activate() never widens granted permissions without explicit consent (409 CONSENT_REQUIRED); the row toggle + "Update All" route through the consent dialog, which now queues instead of overwriting. Medium/low - egress: gate dgram hostnames through the IP-vetting resolver; block the low-level socket escape (process.binding) + lock the wrapped prototypes; canonicalize IPv6 in isBlockedIp (hex-mapped/compressed metadata); reject degenerate `*.` / whole-TLD / spaced outbound hosts in the manifest + CSP. - ws:broadcast is membership-gated to the acting user's trips / own connections; users.getById is scoped to users the acting user can see (no enumeration). - safe-fetch streams + aborts at the byte cap (chunked codeload OOM); isPrivateIp reuses the canonicalizing check. - native-scan throws instead of silently passing past its entry cap. - SDK: dev serves binary assets as raw buffers + handles EADDRINUSE; manifest validator gains the reserved-id + outbound-host checks; wikis corrected. Tests updated for the new membership-gated behaviour + regression tests added (IPv6 canonicalization, wildcard hardening, outbound-host validation, ws/user scoping). 234 plugin tests + 24 SDK tests green.
75 lines
3.8 KiB
TypeScript
75 lines
3.8 KiB
TypeScript
/**
|
|
* trek-plugin.json validation (#plugins, M4). Rejects invalid ids/versions/types,
|
|
* unknown permissions, native modules, and http:outbound without egress.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { parseManifest, ManifestError } from '../../../src/nest/plugins/install/manifest';
|
|
|
|
const base = { id: 'flight-tracker', name: 'Flight', version: '1.2.0', type: 'widget', apiVersion: 1 };
|
|
|
|
describe('parseManifest', () => {
|
|
it('parses a valid manifest with defaults', () => {
|
|
const m = parseManifest({ ...base });
|
|
expect(m.id).toBe('flight-tracker');
|
|
expect(m.icon).toBe('Blocks');
|
|
expect(m.nativeModules).toBe(false);
|
|
expect(m.permissions).toEqual([]);
|
|
});
|
|
|
|
it('derives minTrekVersion from the trek range', () => {
|
|
expect(parseManifest({ ...base, trek: '>=3.2.0 <4.0.0' }).minTrekVersion).toBe('3.2.0');
|
|
});
|
|
|
|
it('keeps known permissions + parses settings', () => {
|
|
const m = parseManifest({
|
|
...base,
|
|
permissions: ['db:own', 'http:outbound:api.x.com'],
|
|
egress: ['api.x.com'],
|
|
settings: [{ key: 'api_key', input_type: 'password', scope: 'instance', secret: true }, { bad: 1 }],
|
|
});
|
|
expect(m.permissions).toContain('db:own');
|
|
expect(m.settings).toHaveLength(1);
|
|
expect(m.settings[0]).toMatchObject({ key: 'api_key', secret: true, scope: 'instance' });
|
|
});
|
|
|
|
it('accepts a valid exact + wildcard outbound host', () => {
|
|
const m = parseManifest({ ...base, permissions: ['http:outbound:api.x.com', 'http:outbound:*.example.com'], egress: ['api.x.com', '*.example.com'] });
|
|
expect(m.permissions).toContain('http:outbound:*.example.com');
|
|
});
|
|
|
|
it.each([
|
|
['Bad-Id', { ...base, id: 'Bad-Id' }, /invalid id/],
|
|
['reserved id', { ...base, id: 'registry' }, /reserved id/],
|
|
['bad version', { ...base, version: '1.x' }, /invalid version/],
|
|
['bad type', { ...base, type: 'thing' }, /invalid type/],
|
|
['native', { ...base, nativeModules: true }, /native modules/],
|
|
['unknown perm', { ...base, permissions: ['fs:read'] }, /unknown permission/],
|
|
['outbound no egress', { ...base, permissions: ['http:outbound'] }, /egress\[\] is empty/],
|
|
['wildcard egress', { ...base, permissions: ['http:outbound'], egress: ['*'] }, /bare "\*"/],
|
|
['allow-all outbound host', { ...base, permissions: ['http:outbound:*'], egress: ['x.com'] }, /invalid http:outbound host/],
|
|
['degenerate wildcard host', { ...base, permissions: ['http:outbound:*.'], egress: ['x.com'] }, /invalid http:outbound host/],
|
|
['whole-TLD outbound host', { ...base, permissions: ['http:outbound:*.com'], egress: ['x.com'] }, /invalid http:outbound host/],
|
|
['host with a space', { ...base, permissions: ['http:outbound:legit.com x'], egress: ['legit.com'] }, /invalid http:outbound host/],
|
|
['bad egress host', { ...base, permissions: ['http:outbound:api.x.com'], egress: ['api.x.com', 'no spaces here'] }, /invalid egress host/],
|
|
['not an object', 'nope', /not an object/],
|
|
['missing name', { id: 'x-plugin', version: '1.0.0', type: 'page' }, /missing\/invalid "name"/],
|
|
])('rejects: %s', (_label, input, re) => {
|
|
expect(() => parseManifest(input)).toThrow(ManifestError);
|
|
expect(() => parseManifest(input)).toThrow(re as RegExp);
|
|
});
|
|
});
|
|
|
|
describe('parseManifest capabilities', () => {
|
|
it('parses a hero widget slot and defaults to sidebar', () => {
|
|
const hero = parseManifest({ ...base, capabilities: { widget: { slot: 'hero', title: 'T' } } });
|
|
expect(hero.capabilities.widget?.slot).toBe('hero');
|
|
const plain = parseManifest({ ...base, capabilities: { widget: {} } });
|
|
expect(plain.capabilities.widget?.slot).toBe('sidebar');
|
|
expect(parseManifest(base).capabilities).toEqual({});
|
|
});
|
|
|
|
it('rejects an unknown widget slot', () => {
|
|
expect(() => parseManifest({ ...base, capabilities: { widget: { slot: 'floating' } } })).toThrow(ManifestError);
|
|
});
|
|
});
|