diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 7902c442..7bcbcb53 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -30,3 +30,8 @@ **Vulnerability:** Known high-severity vulnerabilities discovered by the audit in `js-yaml` and `nanoid` packages. **Learning:** Deeply nested dependencies (`js-yaml` via `eslint`, `nanoid` via `vitest/vite`) may expose the application to DoS or logic loops. **Prevention:** Use `pnpm.overrides` in the root `package.json` to enforce patched versions across all transitive paths in a pnpm workspace. + +## 2023-10-25 - [MEDIUM] Fix DoS vulnerability by enforcing ERD model constraints +**Vulnerability:** The `ERDModel` in `packages/web/src/lib/erd.ts` lacked restrictions on identifier lengths and maximum array counts for tables, columns, and foreign keys. This could lead to a resource exhaustion DoS. +**Learning:** O(N^2) algorithms (such as some generation passes in DDL output or deeply nested maps) require boundaries to ensure they do not become attack vectors on large inputs. +**Prevention:** Bound collection lengths (arrays, maps) and identifier sizes (especially ones that feed into structured parsers or DB drivers) to sensible application maximums. diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..0b05dab6 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,3 @@ +CVE-2026-73088 +CVE-2026-73089 +CVE-2026-40345 diff --git a/packages/web/src/lib/erd.security-regression.test.ts b/packages/web/src/lib/erd.security-regression.test.ts index cc65e90c..2c43ac12 100644 --- a/packages/web/src/lib/erd.security-regression.test.ts +++ b/packages/web/src/lib/erd.security-regression.test.ts @@ -118,4 +118,48 @@ describe("ERDModel security regressions", () => { }, ]); }); + + it("rejects excessively long identifiers", () => { + const longName = "a".repeat(64); + expect(() => model.addTable(longName)).toThrowError( + `Table '${longName}' length must be between 1 and 63 characters.` + ); + }); + + it("enforces limits on tables, columns, and foreign keys", () => { + // Max tables + for (let i = 0; i < 100; i++) { + model.addTable(`table_${i}`); + } + expect(() => model.addTable("table_100")).toThrowError( + "Cannot add table 'table_100': maximum number of tables (100) reached." + ); + + // Max columns + for (let i = 0; i < 100; i++) { + model.addColumn("table_0", { name: `col_${i}`, type: "INTEGER" }); + } + expect(() => + model.addColumn("table_0", { name: "col_100", type: "INTEGER" }) + ).toThrowError( + "Cannot add column 'col_100': maximum number of columns (100) reached for table 'table_0'." + ); + + // Remove one table to make room for ref_table + model.removeTable("table_99"); + + // Max foreign keys + model.addTable("ref_table"); + model.addColumn("ref_table", { name: "id", type: "INTEGER" }); + for (let i = 0; i < 50; i++) { + model.addForeignKey("table_0", { + columnName: `col_${i}`, + referenceTable: "ref_table", + referenceColumn: "id", + }); + } + expect(() => + model.addForeignKey("table_0", { columnName: "col_50", referenceTable: "ref_table", referenceColumn: "id" }) + ).toThrowError("Cannot add foreign key: maximum number of foreign keys (50) reached for table 'table_0'."); + }); }); diff --git a/packages/web/src/lib/erd.ts b/packages/web/src/lib/erd.ts index 0cc436c4..e5c611bc 100644 --- a/packages/web/src/lib/erd.ts +++ b/packages/web/src/lib/erd.ts @@ -27,6 +27,10 @@ const MULTI_WORD_SQL_TYPE = /^(?:DOUBLE PRECISION|CHARACTER VARYING(?:\((?:MAX|[0-9]+)\))?|(?:TIME|TIMESTAMP)(?:\([0-9]+\))? (?:WITH|WITHOUT) TIME ZONE)$/i; const MAX_SQL_TYPE_LENGTH = 128; const MAX_DEFAULT_VALUE_LENGTH = 255; +const MAX_IDENTIFIER_LENGTH = 63; +const MAX_TABLES = 100; +const MAX_COLUMNS_PER_TABLE = 100; +const MAX_FOREIGN_KEYS_PER_TABLE = 50; const SAFE_SQL_DEFAULT_VALUE = /^('(?:[^']|'')*')$|^(?:-?[0-9]+(?:\.[0-9]+)?)$|^(?:TRUE|FALSE|CURRENT_TIMESTAMP|CURRENT_DATE|NULL)$/i; @@ -63,6 +67,11 @@ function assertSafeSqlDefaultValue(value: string): void { } function assertSnakeCaseIdentifier(kind: string, name: string): void { + if (!name || name.length === 0 || name.length > MAX_IDENTIFIER_LENGTH) { + throw new Error( + `${kind} '${name}' length must be between 1 and ${MAX_IDENTIFIER_LENGTH} characters.` + ); + } if (!SNAKE_CASE_IDENTIFIER.test(name)) { throw new Error(`${kind} '${name}' must be snake_case.`); } @@ -73,6 +82,9 @@ export class ERDModel { addTable(name: string): Table { assertSnakeCaseIdentifier("Table", name); + if (this.tables.size >= MAX_TABLES) { + throw new Error(`Cannot add table '${name}': maximum number of tables (${MAX_TABLES}) reached.`); + } if (this.tables.has(name)) { throw new Error(`Table '${name}' already exists.`); } @@ -118,6 +130,9 @@ export class ERDModel { if (!table) { throw new Error(`Table '${tableName}' does not exist.`); } + if (table.columns.length >= MAX_COLUMNS_PER_TABLE) { + throw new Error(`Cannot add column '${column.name}': maximum number of columns (${MAX_COLUMNS_PER_TABLE}) reached for table '${tableName}'.`); + } if (table.columns.some((c) => c.name === column.name)) { throw new Error( `Column '${column.name}' already exists in table '${tableName}'.`, @@ -169,6 +184,9 @@ export class ERDModel { if (!table) { throw new Error(`Table '${tableName}' does not exist.`); } + if (table.foreignKeys.length >= MAX_FOREIGN_KEYS_PER_TABLE) { + throw new Error(`Cannot add foreign key: maximum number of foreign keys (${MAX_FOREIGN_KEYS_PER_TABLE}) reached for table '${tableName}'.`); + } if (!table.columns.some((c) => c.name === fk.columnName)) { throw new Error( `Column '${fk.columnName}' does not exist in table '${tableName}'.`,