Skip to content
Open
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: 24 additions & 0 deletions src/commands/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export const browserCommand = new Command("browser")
.description("Open web UI for task management")
.option("-p, --port <port>", "Port number", String(DEFAULT_PORT))
.option("--no-open", "Don't open browser automatically")
.option("--cors-origin <origin>", "Custom CORS origin(s) for development (comma-separated)")
.option("--cors-credentials", "Enable CORS credentials (default: true)", true)
.option("--strict-cors", "Use strict CORS (localhost only, no dev origins)")
.action(async (options) => {
const projectRoot = findProjectRoot();
if (!projectRoot) {
Expand Down Expand Up @@ -143,14 +146,35 @@ export const browserCommand = new Command("browser")
// Always save port to config so notify-server stays in sync
await saveServerPort(projectRoot, port);

// Configure CORS options
let corsOrigin: string | string[] | undefined;

if (options.strictCors) {
// Strict mode: localhost only
corsOrigin = "http://localhost:3000";
} else if (options.corsOrigin) {
// Custom origins from CLI
corsOrigin = options.corsOrigin.split(",").map((o: string) => o.trim());
}
// else: use default dev origins (localhost:3000, localhost:5173, etc.)

console.log(chalk.cyan("β—† Starting Knowns.dev Web UI..."));
if (options.corsOrigin) {
console.log(chalk.gray(` CORS origins: ${options.corsOrigin}`));
} else if (options.strictCors) {
console.log(chalk.gray(" CORS: Strict mode (localhost:3000 only)"));
} else {
console.log(chalk.gray(" CORS: Development mode (localhost + common dev ports)"));
}
console.log("");

try {
await startServer({
port,
projectRoot,
open: options.open,
corsOrigin,
corsCredentials: options.corsCredentials,
});

console.log("");
Expand Down
25 changes: 14 additions & 11 deletions src/import/providers/git.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* Git Import Provider
* Git Import Provider - SECURED VERSION
*
* Imports from git repositories using sparse checkout when possible.
* Security fix: Replaced execSync with spawnSync to prevent command injection
*/

import { execSync, spawnSync } from "node:child_process";
Expand Down Expand Up @@ -148,37 +149,39 @@ export class GitProvider extends ImportProvider {

/**
* Sparse checkout - only fetch .knowns/ directory
* SECURITY FIX: Use spawnSync with args array instead of execSync to prevent command injection
*/
private async sparseCheckout(source: string, tempDir: string, ref: string): Promise<void> {
// Initialize repo with sparse checkout
execSync("git init", { cwd: tempDir, stdio: "pipe" });
execSync(`git remote add origin "${source}"`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["init"], { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["remote", "add", "origin", source], { cwd: tempDir, stdio: "pipe" });

// Configure sparse checkout
execSync("git config core.sparseCheckout true", { cwd: tempDir, stdio: "pipe" });
execSync("git sparse-checkout init --cone", { cwd: tempDir, stdio: "pipe" });
execSync(`git sparse-checkout set ${KNOWNS_DIR}`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["config", "core.sparseCheckout", "true"], { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["sparse-checkout", "init", "--cone"], { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["sparse-checkout", "set", KNOWNS_DIR], { cwd: tempDir, stdio: "pipe" });

// Fetch only what we need
const fetchRef = ref === "HEAD" ? "HEAD" : ref;
execSync(`git fetch --depth=1 origin ${fetchRef}`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["fetch", "--depth=1", "origin", fetchRef], { cwd: tempDir, stdio: "pipe" });

// Checkout
if (ref === "HEAD") {
execSync("git checkout FETCH_HEAD", { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["checkout", "FETCH_HEAD"], { cwd: tempDir, stdio: "pipe" });
} else {
execSync(`git checkout ${ref}`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["checkout", ref], { cwd: tempDir, stdio: "pipe" });
}
}

/**
* Full clone - fallback for older git versions
* SECURITY FIX: Use spawnSync with args array instead of execSync to prevent command injection
*/
private async fullClone(source: string, tempDir: string, ref: string): Promise<void> {
if (ref === "HEAD") {
execSync(`git clone --depth=1 "${source}" .`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["clone", "--depth=1", source, "."], { cwd: tempDir, stdio: "pipe" });
} else {
execSync(`git clone --depth=1 --branch "${ref}" "${source}" .`, { cwd: tempDir, stdio: "pipe" });
spawnSync("git", ["clone", "--depth=1", "--branch", ref, source, "."], { cwd: tempDir, stdio: "pipe" });
}
}

Expand Down
24 changes: 18 additions & 6 deletions src/search/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export class SearchStore {

/**
* Search for similar chunks using sqlite-vec native vector search
* SECURITY FIX: Use parameterized queries to prevent SQL injection
*/
search(
queryEmbedding: number[],
Expand Down Expand Up @@ -310,15 +311,18 @@ export class SearchStore {
WHERE v.embedding MATCH ? AND k = ${k}
`;

const params: unknown[] = [embeddingBlob];

if (type !== "all") {
sql += ` AND c.type = '${type}'`;
sql += " AND c.type = ?";
params.push(type);
}

sql += `
ORDER BY v.distance
`;

const rows = this.db.prepare(sql).all(embeddingBlob) as Array<{
const rows = this.db.prepare(sql).all(...params) as Array<{
vec_rowid: number;
distance: number;
id: string;
Expand Down Expand Up @@ -405,14 +409,18 @@ export class SearchStore {

/**
* Get all chunks (for keyword search fallback)
* SECURITY FIX: Use parameterized queries to prevent SQL injection
*/
getAllChunks(type?: "doc" | "task"): Chunk[] {
let sql = "SELECT * FROM chunks";
const params: unknown[] = [];

if (type) {
sql += ` WHERE type = '${type}'`;
sql += " WHERE type = ?";
params.push(type);
}

const rows = this.db.prepare(sql).all() as Array<{
const rows = this.db.prepare(sql).all(...params) as Array<{
id: string;
type: string;
content: string;
Expand All @@ -434,13 +442,17 @@ export class SearchStore {

/**
* Count chunks
* SECURITY FIX: Use parameterized queries to prevent SQL injection
*/
count(type?: "doc" | "task"): number {
let sql = "SELECT COUNT(*) as count FROM chunks";
const params: unknown[] = [];

if (type) {
sql += ` WHERE type = '${type}'`;
sql += " WHERE type = ?";
params.push(type);
}
const result = this.db.prepare(sql).get() as { count: number };
const result = this.db.prepare(sql).get(...params) as { count: number };
return result.count;
}

Expand Down
19 changes: 18 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ interface ServerOptions {
port: number;
projectRoot: string;
open: boolean;
corsOrigin?: string | string[]; // Custom CORS origin(s) for development
corsCredentials?: boolean; // Enable credentials for CORS (default: true)
}

export async function startServer(options: ServerOptions) {
Expand Down Expand Up @@ -88,7 +90,22 @@ export async function startServer(options: ServerOptions) {

// Create Express app
const app = express();
app.use(cors());

// SECURITY FIX: Restrict CORS with flexible configuration for development
// Default: localhost only (secure)
// Dev mode: Allow custom origins via options
const corsOptions = {
origin: options.corsOrigin || [
"http://localhost:3000",
"http://127.0.0.1:3000",
// Allow common Vite dev ports
"http://localhost:5173",
"http://127.0.0.1:5173",
],
credentials: options.corsCredentials ?? true,
};

app.use(cors(corsOptions));
app.use(express.json());

// Serve static assets from Vite build (assets folder with hashed names)
Expand Down