Security: - stash_page: reject path-traversal / percent-encoded srcs before the authed loopback fetch (resolveInternalFilePath), closing an SSRF/exfiltration hole where a crafted node.attrs.src could read an arbitrary internal GET endpoint into the anonymous sandbox. Stability: - stash_page: revert + recount mirrors FIFO-evicted by a later put in the same stash (no dangling sandbox refs, honest images.mirrored/failed); free image blobs if the final document put throws. - Reject/clamp non-positive SANDBOX_TTL_MS to the 1h default (warn once). - Log mirror failures unconditionally (console.warn, no blob bodies). Cleanup / architecture: - Remove dead expiresAt from SandboxPutResult. - Centralize the /api/sb route in SANDBOX_ROUTE_SEGMENT/SANDBOX_API_PATH and move URL composition into SandboxStore.putAndLink; drop the duplicated sink closures and the now-unused EnvironmentService injection from McpService and AiChatToolsService. - Un-export isInternalFileUrl; document the process-local (instance-bound) sandbox limitation in the tool description and .env.example. Docs/tests: - README/README.ru: 38 -> 39 tools + stash_page entry. - Add traversal/normalize/recursion unit tests, stash self-eviction + doc-put-throw + empty/octet-stream mock tests, controller If-None-Match (wildcard/weak/list) + Cache-Control tests, and SANDBOX_TTL_MS validation tests. Regenerate packages/mcp/build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,4 +14,30 @@ describe('EnvironmentService', () => {
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getSandboxTtlMs', () => {
|
||||
// ConfigService stub: get(key, def) returns the configured value for the key
|
||||
// (falling back to def), matching the @nestjs/config contract the service
|
||||
// calls with (key, default).
|
||||
const build = (sandboxTtl?: string) =>
|
||||
new EnvironmentService({
|
||||
get: (key: string, def?: string) =>
|
||||
key === 'SANDBOX_TTL_MS' ? (sandboxTtl ?? def) : def,
|
||||
} as any);
|
||||
|
||||
it.each(['0', '-5', 'abc'])(
|
||||
'falls back to the 3600000 default for invalid value %s',
|
||||
(value) => {
|
||||
expect(build(value).getSandboxTtlMs()).toBe(3_600_000);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns the parsed value for a valid positive integer', () => {
|
||||
expect(build('120000').getSandboxTtlMs()).toBe(120_000);
|
||||
});
|
||||
|
||||
it('uses the 3600000 default when SANDBOX_TTL_MS is unset', () => {
|
||||
expect(build(undefined).getSandboxTtlMs()).toBe(3_600_000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import ms, { StringValue } from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class EnvironmentService {
|
||||
private readonly logger = new Logger(EnvironmentService.name);
|
||||
// One-shot guard so an invalid SANDBOX_TTL_MS is warned about once, not on
|
||||
// every getSandboxTtlMs() call (which runs per blob put).
|
||||
private sandboxTtlWarned = false;
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getNodeEnv(): string {
|
||||
@@ -348,12 +353,25 @@ export class EnvironmentService {
|
||||
}
|
||||
|
||||
// Blob time-to-live. Default 1h. The unguessable UUID + this short TTL + TLS
|
||||
// are the whole capability model (no tokens).
|
||||
// are the whole capability model (no tokens). A non-positive or non-integer
|
||||
// value would make every blob expire instantly (silent 404s), so reject it and
|
||||
// fall back to the 1h default (warned about once to avoid per-put log spam).
|
||||
getSandboxTtlMs(): number {
|
||||
return parseInt(
|
||||
const parsed = parseInt(
|
||||
this.configService.get<string>('SANDBOX_TTL_MS', '3600000'),
|
||||
10,
|
||||
);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
if (!this.sandboxTtlWarned) {
|
||||
this.sandboxTtlWarned = true;
|
||||
this.logger.warn(
|
||||
`Invalid SANDBOX_TTL_MS (must be a positive integer); ` +
|
||||
`falling back to the 3600000 ms default`,
|
||||
);
|
||||
}
|
||||
return 3_600_000;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Per-blob cap for non-image blobs (the serialized document). Default 8 MiB.
|
||||
|
||||
Reference in New Issue
Block a user