Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 10 additions & 14 deletions src/embeddings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PluginConfig } from './types.js';
import type { PluginConfig } from './types.js';
import { requestRemoteEmbedding } from './embedding-provider-client.js';
import {
averageEmbeddings,
Expand Down Expand Up @@ -58,21 +58,17 @@ export class EmbeddingGenerator {
return results;
}

private async generateChunk(chunk: EmbeddingChunk): Promise<number[]> {
private generateChunk(chunk: EmbeddingChunk): Promise<number[]> {
if (this.provider === 'hash') {
return hashEmbedding(chunk.content, this.dimensions);
}
try {
return await requestRemoteEmbedding({
provider: this.provider,
model: this.config.embeddingModel,
dimensions: this.dimensions,
apiKey: this.config.embeddingApiKey,
apiUrl: this.config.embeddingApiUrl,
}, chunk.content);
} catch {
return hashEmbedding(chunk.content, this.dimensions);
return Promise.resolve(hashEmbedding(chunk.content, this.dimensions));
}
return requestRemoteEmbedding({
provider: this.provider,
model: this.config.embeddingModel,
dimensions: this.dimensions,
apiKey: this.config.embeddingApiKey,
apiUrl: this.config.embeddingApiUrl,
}, chunk.content);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/redactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@
}

try {
const trimmed = text.trim();
if (this.config.categories.path !== 'off' && looksLikeStandalonePath(trimmed)) {
return this.redactPath(text);
}
return this.redactUnsafe(text);
} catch {
// Fail-closed: redact everything we safely can
Expand Down Expand Up @@ -418,6 +422,16 @@
return pathStyle(value) === 'windows' ? win32.isAbsolute(value) : posix.isAbsolute(value);
}

function looksLikeStandalonePath(value: string): boolean {
if (!isAbsolutePath(value) || /\s--?[A-Za-z0-9]/u.test(value)) return false;
const absolutePathStarts = value.match(
/(?:^|\s)(?:[A-Za-z]:[\\/]|\\\\|\/(?:home|Users|root|var|opt|etc|tmp|usr|mnt|srv|data|www|private|Volumes|Library|Applications)(?:\/|$))/gu,
);
if ((absolutePathStarts?.length ?? 0) > 1) return false;
if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('\\\\')) return true;
return /^\/(?:home|Users|root|var|opt|etc|tmp|usr|mnt|srv|data|www|private|Volumes|Library|Applications)(?:\/.+)+$/u.test(value);

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var' and with many repetitions of '//'.
This
regular expression
that depends on
library input
may run slow on strings starting with '/var/' and with many repetitions of '//'.
This
regular expression
that de

Check failure

Code scanning / CodeQL

Inefficient regular expression High

This part of the regular expression may cause exponential backtracking on strings starting with '/var/' and containing many repetitions of 'a/'.
}

/**
* Convenience function: redact a string with default config.
*/
Expand Down
34 changes: 28 additions & 6 deletions src/wiki-export-files.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
import { dirname, posix, win32 } from 'node:path';
import { renderLogEntry, type LogEntry, type RenderedNote } from './wiki-note-renderer.js';
import type { WikiExportManifest } from './wiki-export-types.js';

Expand Down Expand Up @@ -55,18 +55,40 @@ export function pruneOwnedFiles(outputDir: string, paths: string[]): number {
}

export function resolveOwnedPath(outputDir: string, manifestPath: string): string {
if (!manifestPath || isAbsolute(manifestPath)) {
const windowsRoot = isWindowsPath(outputDir)
|| (!posix.isAbsolute(outputDir) && process.platform === 'win32');
const pathApi = windowsRoot ? win32 : posix;
const containsTraversal = manifestPath.split(/[\\/]/u).includes('..');

if (
!manifestPath
|| manifestPath.includes('\0')
|| containsTraversal
|| /^[A-Za-z]:/u.test(manifestPath)
|| win32.isAbsolute(manifestPath)
|| posix.isAbsolute(manifestPath)
) {
throw new Error(`Unsafe wiki manifest path: ${manifestPath}`);
}
const root = resolve(outputDir);
const candidate = resolve(root, manifestPath);
const fromRoot = relative(root, candidate);
if (!fromRoot || fromRoot.startsWith('..') || isAbsolute(fromRoot)) {

const root = pathApi.resolve(outputDir);
const candidate = pathApi.resolve(root, manifestPath);
const fromRoot = pathApi.relative(root, candidate);
if (
!fromRoot
|| fromRoot === '..'
|| fromRoot.startsWith(`..${pathApi.sep}`)
|| pathApi.isAbsolute(fromRoot)
) {
throw new Error(`Unsafe wiki manifest path: ${manifestPath}`);
}
return candidate;
}

function isWindowsPath(value: string): boolean {
return /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('\\\\');
}

export function appendExportLog(outputDir: string, entry: LogEntry): void {
const logPath = resolveOwnedPath(outputDir, 'log.md');
const existing = existsSync(logPath) ? readFileSync(logPath, 'utf8') : '';
Expand Down
4 changes: 2 additions & 2 deletions test/backfill-recall-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe('Phase 19b — backfill and recall telemetry', () => {
databaseUrl: buildTempDbUrl(BASE_DB_URL, tempDbName),
databaseProvider: 'postgres',
sqlitePath: '.data/csm-memory.db',
embeddingModel: 'nomic-embed-text',
embeddingApiUrl: process.env.OLLAMA_URL ?? 'http://localhost:11434',
embeddingModel: 'test-hash',
embeddingDimensions: 768,
} as PluginConfig;

let db: Database;
Expand Down
4 changes: 2 additions & 2 deletions test/hybrid-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ async function cleanupDatabase(
databaseUrl: databaseUrl(databaseName),
databaseProvider: 'postgres',
sqlitePath: '.data/csm-memory.db',
embeddingModel: 'nomic-embed-text',
embeddingApiUrl: process.env.OLLAMA_URL ?? 'http://localhost:11434',
embeddingModel: 'test-hash',
embeddingDimensions: 768,
} as PluginConfig;
let db: Database | undefined;
let embeddings: EmbeddingGenerator;
Expand Down
Loading