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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CVE-2026-73088
CVE-2026-73089
CVE-2026-40345
44 changes: 44 additions & 0 deletions packages/web/src/lib/erd.security-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'.");
});
});
18 changes: 18 additions & 0 deletions packages/web/src/lib/erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.`);
}
Expand All @@ -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.`);
}
Expand Down Expand Up @@ -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}'.`,
Expand Down Expand Up @@ -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}'.`,
Expand Down
Loading