Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,14 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server
],
{ concurrency: "unbounded" },
);
// Logs, traces and the database are written with the process umask; keep them owner-only.
yield* fs
.chmod(derivedPaths.stateDir, 0o700)
.pipe(
Effect.catch((cause) =>
Effect.logWarning("Could not restrict state directory permissions.", { cause }),
),
);

const swept = sweepStalePendingAttachments({
attachmentsDir: derivedPaths.attachmentsDir,
Expand Down
41 changes: 41 additions & 0 deletions packages/shared/src/observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,47 @@ describe("observability", () => {
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("redacts WebSocket credentials from recorded and exported URLs", () =>
Effect.gen(function* () {
const delegates: Array<Tracer.NativeSpan> = [];
const records: Array<EffectTraceRecord> = [];
const tracer = yield* makeLocalFileTracer({
filePath: "unused",
maxBytes: 1024,
maxFiles: 1,
batchWindowMs: 10_000,
sink: {
filePath: "unused",
push: (record) => {
if (record.type === "effect-span") records.push(record);
},
flush: Effect.void,
close: () => Effect.void,
},
delegate: Tracer.make({
span: (options) => {
const span = new Tracer.NativeSpan(options);
delegates.push(span);
return span;
},
}),
});
yield* Effect.annotateCurrentSpan({
"url.full": "http://127.0.0.1:3773/ws?wsTicket=ticket-secret&clientSurface=web",
"url.query": "dpop=proof-secret&wsTicket=ticket-secret",
"url.path": "/ws",
}).pipe(Effect.withSpan("GET"), Effect.provideService(Tracer.Tracer, tracer));

const expected = {
"url.full": "http://127.0.0.1:3773/ws?wsTicket=<redacted>&clientSurface=web",
"url.query": "dpop=<redacted>&wsTicket=<redacted>",
"url.path": "/ws",
};
assert.deepInclude(records[0]?.attributes, expected);
assert.deepInclude(Object.fromEntries(delegates[0]!.attributes), expected);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("normalizes circular arrays, maps, and sets without recursing forever", () => {
const array: Array<unknown> = ["alpha"];
array.push(array);
Expand Down
15 changes: 13 additions & 2 deletions packages/shared/src/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,16 @@ export const makeTraceSink = Effect.fn("makeTraceSink")(function* (options: Trac
} satisfies TraceSink;
});

// WebSocket tickets and their DPoP proofs ride in the query and stay valid after the request.
const URL_CREDENTIAL_PARAMS = /(^|[?&])(wsTicket|dpop)=[^&#]*/g;

function redactUrlCredentials(key: string, value: unknown): unknown {
if ((key !== "url.full" && key !== "url.query") || typeof value !== "string") {
return value;
}
return value.replace(URL_CREDENTIAL_PARAMS, "$1$2=<redacted>");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved

class LocalFileSpan implements Tracer.Span {
readonly _tag = "Span";
readonly name: string;
Expand Down Expand Up @@ -598,8 +608,9 @@ class LocalFileSpan implements Tracer.Span {
}

attribute(key: string, value: unknown): void {
this.attributes.set(key, value);
this.delegate.attribute(key, value);
const recorded = redactUrlCredentials(key, value);
this.attributes.set(key, recorded);
this.delegate.attribute(key, recorded);
}

event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void {
Expand Down
Loading