gitnexus 1.6.4-rc.54 → 1.6.4-rc.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -132,17 +132,25 @@ export function c3Linearize(classId, parentMap, cache, inProgress) {
132
132
  }
133
133
  // Add the direct parents list as the final sequence
134
134
  const sequences = [...parentLinearizations, [...directParents]];
135
+ const heads = new Uint32Array(sequences.length); // head pointer per sequence
135
136
  const result = [];
137
+ // Tail-count map: how many sequences contain this id at index > head.
138
+ // O(1) membership check replaces O(n) indexOf scans.
139
+ const tailCount = new Map();
140
+ for (const seq of sequences) {
141
+ for (let i = 1; i < seq.length; i++) {
142
+ tailCount.set(seq[i], (tailCount.get(seq[i]) ?? 0) + 1);
143
+ }
144
+ }
145
+ let remaining = sequences.reduce((n, s) => n + s.length, 0);
136
146
  let inconsistent = false;
137
- while (sequences.some((s) => s.length > 0)) {
138
- // Find a good head: one that doesn't appear in the tail of any other sequence
147
+ while (remaining > 0) {
139
148
  let head = null;
140
- for (const seq of sequences) {
141
- if (seq.length === 0)
149
+ for (let si = 0; si < sequences.length; si++) {
150
+ if (heads[si] >= sequences[si].length)
142
151
  continue;
143
- const candidate = seq[0];
144
- const inTail = sequences.some((other) => other.length > 1 && other.indexOf(candidate, 1) !== -1);
145
- if (!inTail) {
152
+ const candidate = sequences[si][heads[si]];
153
+ if ((tailCount.get(candidate) ?? 0) === 0) {
146
154
  head = candidate;
147
155
  break;
148
156
  }
@@ -152,10 +160,22 @@ export function c3Linearize(classId, parentMap, cache, inProgress) {
152
160
  break;
153
161
  }
154
162
  result.push(head);
155
- // Remove the chosen head from all sequences
156
- for (const seq of sequences) {
157
- if (seq.length > 0 && seq[0] === head) {
158
- seq.shift();
163
+ // Advance head pointers past the chosen head; update tail counts
164
+ for (let si = 0; si < sequences.length; si++) {
165
+ if (heads[si] >= sequences[si].length)
166
+ continue;
167
+ if (sequences[si][heads[si]] === head) {
168
+ heads[si]++;
169
+ remaining--;
170
+ // promoted was in this sequence's active tail; now it's the new head — remove from tailCount
171
+ if (heads[si] < sequences[si].length) {
172
+ const promoted = sequences[si][heads[si]];
173
+ const prev = tailCount.get(promoted);
174
+ if (prev <= 1)
175
+ tailCount.delete(promoted);
176
+ else
177
+ tailCount.set(promoted, prev - 1);
178
+ }
159
179
  }
160
180
  }
161
181
  }
@@ -25,6 +25,7 @@ import { mountMCPEndpoints } from './mcp-http.js';
25
25
  import { fork } from 'child_process';
26
26
  import { fileURLToPath, pathToFileURL } from 'url';
27
27
  import { JobManager } from './analyze-job.js';
28
+ import { assertString, escapeRegExp, BadRequestError } from './validation.js';
28
29
  import { extractRepoName, getCloneDir, cloneOrPull } from './git-clone.js';
29
30
  const _require = createRequire(import.meta.url);
30
31
  const pkg = _require('../../package.json');
@@ -429,6 +430,10 @@ const mountSSEProgress = (app, routePath, jm) => {
429
430
  });
430
431
  };
431
432
  const statusFromError = (err) => {
433
+ // Validation helpers throw BadRequestError / ForbiddenError with a typed
434
+ // .status field — honor it before falling back to message-string matching.
435
+ if (err instanceof BadRequestError)
436
+ return err.status;
432
437
  const msg = String(err?.message ?? '');
433
438
  if (msg.includes('No indexed repositories') || msg.includes('not found'))
434
439
  return 404;
@@ -968,20 +973,33 @@ export const createServer = async (port, host = '127.0.0.1') => {
968
973
  res.status(404).json({ error: 'Repository not found' });
969
974
  return;
970
975
  }
971
- const pattern = req.query.pattern;
972
- if (!pattern) {
976
+ // Type-confusion guard (CodeQL js/type-confusion-through-parameter-tampering):
977
+ // req.query.pattern is `string | string[] | ParsedQs` — without an explicit
978
+ // type check, the `.length` guard below counts array elements instead of
979
+ // characters, allowing arbitrarily long patterns through.
980
+ const rawPattern = req.query.pattern;
981
+ if (rawPattern === undefined) {
973
982
  res.status(400).json({ error: 'Missing "pattern" query parameter' });
974
983
  return;
975
984
  }
976
- // ReDoS protection: reject overly long or dangerous patterns
985
+ const pattern = assertString(rawPattern, 'pattern');
986
+ if (pattern.length === 0) {
987
+ res.status(400).json({ error: 'Missing "pattern" query parameter' });
988
+ return;
989
+ }
990
+ // Length cap: applies to both literal and regex modes as a defense-in-depth
991
+ // bound against pathological input.
977
992
  if (pattern.length > 200) {
978
993
  res.status(400).json({ error: 'Pattern too long (max 200 characters)' });
979
994
  return;
980
995
  }
981
- // Validate regex syntax
996
+ // Treat user input as a literal substring in all cases to prevent
997
+ // regex-injection/ReDoS via attacker-controlled regex syntax.
998
+ const effectivePattern = escapeRegExp(pattern);
999
+ // Validate regex syntax (catches both opt-in user regex and any escapeRegExp bug)
982
1000
  let regex;
983
1001
  try {
984
- regex = new RegExp(pattern, 'gim');
1002
+ regex = new RegExp(effectivePattern, 'gim');
985
1003
  }
986
1004
  catch {
987
1005
  res.status(400).json({ error: 'Invalid regex pattern' });
@@ -1025,7 +1043,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1025
1043
  res.json({ results });
1026
1044
  }
1027
1045
  catch (err) {
1028
- res.status(500).json({ error: err.message || 'Grep failed' });
1046
+ res.status(statusFromError(err)).json({ error: err.message || 'Grep failed' });
1029
1047
  }
1030
1048
  });
1031
1049
  // List all processes
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Server-side input validation helpers.
3
+ *
4
+ * Convention: helpers throw BadRequestError (or its 403 subclass ForbiddenError)
5
+ * when user input fails validation. Existing route handlers wrap their bodies in
6
+ * try/catch and translate the error to res.status(err.status).json({error: err.message}).
7
+ * This pattern was chosen over an asyncHandler middleware to stay compatible with
8
+ * Express 4's non-propagation of async-thrown errors and to match the existing
9
+ * try/catch shape used throughout api.ts.
10
+ *
11
+ * Scope (this PR — U1 of the security remediation plan):
12
+ * - assertString: closes js/type-confusion-through-parameter-tampering (api.ts:1118)
13
+ * - assertSafePath: consolidates the path-traversal guard from api.ts:1067-1077
14
+ * for reuse across other path-injection findings (U2/U3)
15
+ * - escapeRegExp: utility for upcoming regex-injection fix at /api/grep (U5)
16
+ *
17
+ * Helpers added in later units (U3 git-clone hardening, U4 rate-limiting) live
18
+ * in this module too but are introduced with the dependency they require.
19
+ */
20
+ /**
21
+ * Thrown by validation helpers when user input is rejected.
22
+ * Routes catch via existing try/catch and convert with err.status / err.message.
23
+ */
24
+ export declare class BadRequestError extends Error {
25
+ readonly status: number;
26
+ constructor(message: string, status?: number);
27
+ }
28
+ export declare class ForbiddenError extends BadRequestError {
29
+ constructor(message: string);
30
+ }
31
+ /**
32
+ * Type guard for HTTP request parameters that must be a single string.
33
+ *
34
+ * Express's req.query and req.body parsers return `string | string[] | ParsedQs`
35
+ * for any field, but route handlers commonly cast to `string` and operate on
36
+ * `.length`. When the caller passes the same key twice (?x=a&x=b) the value
37
+ * arrives as an array, and a `.length` check intended for the string ends up
38
+ * counting array elements — bypassing length-based guards (CodeQL
39
+ * js/type-confusion-through-parameter-tampering, alert at api.ts:1118).
40
+ *
41
+ * @throws BadRequestError when value is not a string (array, object, undefined, etc.)
42
+ */
43
+ export declare function assertString(value: unknown, fieldName: string): string;
44
+ /**
45
+ * Resolve a user-supplied relative path against an allowed root and verify it
46
+ * stays inside that root. Mirrors the existing guard at api.ts:1067-1077.
47
+ *
48
+ * Returns the absolute resolved path. Rejects empty paths, null bytes, and
49
+ * paths that resolve outside the root (e.g., `../../../etc/passwd`).
50
+ *
51
+ * @throws BadRequestError when the path is empty or contains a null byte
52
+ * @throws ForbiddenError when the resolved path escapes the root
53
+ */
54
+ export declare function assertSafePath(rawPath: string, root: string): string;
55
+ /**
56
+ * Escape regex metacharacters in a user-supplied string so it can be safely
57
+ * embedded as a literal in `new RegExp(...)`. Used by /api/grep's literal mode
58
+ * and any future endpoint that constructs a regex from caller input.
59
+ */
60
+ export declare function escapeRegExp(input: string): string;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Server-side input validation helpers.
3
+ *
4
+ * Convention: helpers throw BadRequestError (or its 403 subclass ForbiddenError)
5
+ * when user input fails validation. Existing route handlers wrap their bodies in
6
+ * try/catch and translate the error to res.status(err.status).json({error: err.message}).
7
+ * This pattern was chosen over an asyncHandler middleware to stay compatible with
8
+ * Express 4's non-propagation of async-thrown errors and to match the existing
9
+ * try/catch shape used throughout api.ts.
10
+ *
11
+ * Scope (this PR — U1 of the security remediation plan):
12
+ * - assertString: closes js/type-confusion-through-parameter-tampering (api.ts:1118)
13
+ * - assertSafePath: consolidates the path-traversal guard from api.ts:1067-1077
14
+ * for reuse across other path-injection findings (U2/U3)
15
+ * - escapeRegExp: utility for upcoming regex-injection fix at /api/grep (U5)
16
+ *
17
+ * Helpers added in later units (U3 git-clone hardening, U4 rate-limiting) live
18
+ * in this module too but are introduced with the dependency they require.
19
+ */
20
+ import path from 'node:path';
21
+ /**
22
+ * Thrown by validation helpers when user input is rejected.
23
+ * Routes catch via existing try/catch and convert with err.status / err.message.
24
+ */
25
+ export class BadRequestError extends Error {
26
+ status;
27
+ constructor(message, status = 400) {
28
+ super(message);
29
+ this.name = 'BadRequestError';
30
+ this.status = status;
31
+ }
32
+ }
33
+ export class ForbiddenError extends BadRequestError {
34
+ constructor(message) {
35
+ super(message, 403);
36
+ this.name = 'ForbiddenError';
37
+ }
38
+ }
39
+ /**
40
+ * Type guard for HTTP request parameters that must be a single string.
41
+ *
42
+ * Express's req.query and req.body parsers return `string | string[] | ParsedQs`
43
+ * for any field, but route handlers commonly cast to `string` and operate on
44
+ * `.length`. When the caller passes the same key twice (?x=a&x=b) the value
45
+ * arrives as an array, and a `.length` check intended for the string ends up
46
+ * counting array elements — bypassing length-based guards (CodeQL
47
+ * js/type-confusion-through-parameter-tampering, alert at api.ts:1118).
48
+ *
49
+ * @throws BadRequestError when value is not a string (array, object, undefined, etc.)
50
+ */
51
+ export function assertString(value, fieldName) {
52
+ if (typeof value !== 'string') {
53
+ if (Array.isArray(value)) {
54
+ throw new BadRequestError(`Parameter "${fieldName}" must be a single string, got an array`);
55
+ }
56
+ throw new BadRequestError(`Parameter "${fieldName}" must be a string`);
57
+ }
58
+ return value;
59
+ }
60
+ /**
61
+ * Resolve a user-supplied relative path against an allowed root and verify it
62
+ * stays inside that root. Mirrors the existing guard at api.ts:1067-1077.
63
+ *
64
+ * Returns the absolute resolved path. Rejects empty paths, null bytes, and
65
+ * paths that resolve outside the root (e.g., `../../../etc/passwd`).
66
+ *
67
+ * @throws BadRequestError when the path is empty or contains a null byte
68
+ * @throws ForbiddenError when the resolved path escapes the root
69
+ */
70
+ export function assertSafePath(rawPath, root) {
71
+ if (rawPath.length === 0) {
72
+ throw new BadRequestError('Path must not be empty');
73
+ }
74
+ if (rawPath.includes('\0')) {
75
+ throw new BadRequestError('Path must not contain null bytes');
76
+ }
77
+ const resolvedRoot = path.resolve(root);
78
+ const fullPath = path.resolve(resolvedRoot, rawPath);
79
+ if (fullPath !== resolvedRoot && !fullPath.startsWith(resolvedRoot + path.sep)) {
80
+ throw new ForbiddenError('Path traversal denied');
81
+ }
82
+ return fullPath;
83
+ }
84
+ /**
85
+ * Escape regex metacharacters in a user-supplied string so it can be safely
86
+ * embedded as a literal in `new RegExp(...)`. Used by /api/grep's literal mode
87
+ * and any future endpoint that constructs a regex from caller input.
88
+ */
89
+ export function escapeRegExp(input) {
90
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.4-rc.54",
3
+ "version": "1.6.4-rc.55",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",