Skip to content
Merged
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
63 changes: 62 additions & 1 deletion packages/engine/src/resource-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,15 +554,74 @@ function normalizeSqlSelector(selector: string): string {
return normalizedSelector.trim();
}

function stripSqlCommentsAndLiterals(sql: string): string {
const sanitizedCharacters = sql.split("");
let state:
| "single-quoted-literal"
| "double-quoted-identifier"
| "line-comment"
| "block-comment"
| undefined;
for (let index = 0; index !== sql.length; index += 1) {
const currentCharacter = sql[index];
const nextCharacter = sql[index + 1];

if (state === "single-quoted-literal") {
sanitizedCharacters[index] = " ";
if (currentCharacter === "'") state = undefined;
continue;
}

if (state === "double-quoted-identifier") {
if (currentCharacter === "\"") state = undefined;
continue;
}

if (state === "line-comment") {
sanitizedCharacters[index] = " ";
if (currentCharacter === "\n") state = undefined;
continue;
}

if (state === "block-comment") {
sanitizedCharacters[index] = " ";
if (currentCharacter === "*" && nextCharacter === "/") {
sanitizedCharacters[index + 1] = " ";
index += 1;
state = undefined;
}
continue;
}

if (currentCharacter === "'") {
sanitizedCharacters[index] = " ";
state = "single-quoted-literal";
} else if (currentCharacter === "\"") {
state = "double-quoted-identifier";
} else if (currentCharacter === "-" && nextCharacter === "-") {
sanitizedCharacters[index] = " ";
state = "line-comment";
} else if (currentCharacter === "/" && nextCharacter === "*") {
sanitizedCharacters[index] = " ";
sanitizedCharacters[index + 1] = " ";
index += 1;
state = "block-comment";
}
}

return sanitizedCharacters.join("");
}

function sqlTableAccesses(text: string): Array<{ access: "read" | "write"; selector: string }> {
const sanitized = stripSqlCommentsAndLiterals(text);
const accesses: Array<{ access: "read" | "write"; selector: string }> = [];
const identifier = String.raw`(?:"[^"]+"|[A-Za-z_][A-Za-z0-9_$]*)(?:\s*\.\s*(?:"[^"]+"|[A-Za-z_][A-Za-z0-9_$]*))*`;
const sqlPattern = new RegExp(
String.raw`\b(delete\s+from|insert\s+into|merge\s+into|truncate(?:\s+table)?|drop\s+table(?:\s+if\s+exists)?|create\s+table(?:\s+if\s+not\s+exists)?|alter\s+table|from|join|update)\s+(${identifier})`,
"gi",
);
let match: RegExpExecArray | null;
while ((match = sqlPattern.exec(text)) !== null) {
while ((match = sqlPattern.exec(sanitized)) !== null) {
const verb = match[1].toLowerCase();
const selector = normalizeSqlSelector(match[2]);
accesses.push({ access: verb === "from" || verb === "join" ? "read" : "write", selector });
Expand Down Expand Up @@ -1802,5 +1861,7 @@ export const resourceAccessTestHooks = {
routeReceiverLooksHttp,
queueReceiverLooksExternal,
selectorLooksQueueTopic,
sqlTableAccesses,
stripSqlCommentsAndLiterals,
};
// Stryker restore all
94 changes: 94 additions & 0 deletions tests/resource-access-coverage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2286,3 +2286,97 @@ test("bug #52 supported Fastify route methods do not depend on scan hints", (tes
}
}
});

test("bug #77 FROM in SQL string literals and comments is not mistaken for a table reference", () => {
const { sqlTableAccesses } = resourceAccessTestHooks;
assert.deepEqual(sqlTableAccesses("SELECT 'FROM ghost_table' AS label"), []);
assert.deepEqual(sqlTableAccesses("SELECT 1 /* FROM ghost_table */"), []);
assert.deepEqual(sqlTableAccesses("SELECT 1 -- FROM ghost_table"), []);
assert.deepEqual(sqlTableAccesses("SELECT id FROM actual_table"), [
{ access: "read", selector: "actual_table" },
]);
assert.deepEqual(
sqlTableAccesses("SELECT id FROM actual_table WHERE note = 'FROM false_table' /* JOIN ignored_table */"),
[{ access: "read", selector: "actual_table" }],
);
});

test("bug #77 SQL masking preserves lexical boundaries and table separators", () => {
const { sqlTableAccesses, stripSqlCommentsAndLiterals } = resourceAccessTestHooks;
const readAccesses = (...selectors) => selectors.map((selector) => ({ access: "read", selector }));
const cases = [
{
sql: "SELECT '-- literal', '/*', '*/', 'it''s FROM ghost' FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 'abc\\' FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT '/*' AS marker FROM swallowed_table WHERE note = '*/'",
expected: readAccesses("swallowed_table"),
},
{
sql: "SELECT'x'FROM/* JOIN ghost */actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 1 -- ' FROM ghost_one\nFROM actual_table\n-- JOIN ghost_two",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 1 -- FROM ghost_one\rFROM actual_table",
expected: [],
},
{
sql: "SELECT 1 -- FROM ghost_one\r\nFROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: 'SELECT * FROM "orders--archive" JOIN "items/*current*/"',
expected: readAccesses("orders--archive", "items/*current*/"),
},
{
sql: 'SELECT * FROM "actual_table" /* JOIN ghost_table */',
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 1 /* ' -- FROM ghost */ FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT * /*/*/ FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 1/*x*/*2 FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 8 / 2 * 3 - 1 FROM actual_table",
expected: readAccesses("actual_table"),
},
{
sql: "SELECT 1 /* 8 / 2 * 3 FROM ghost_table */ FROM actual_table",
expected: readAccesses("actual_table"),
},
{ sql: "SELECT 1 /* FROM ghost_table", expected: [] },
{
sql: "SELECT 1/*/ FROM ghost_table */ FROM actual_table",
expected: readAccesses("actual_table"),
},
{ sql: "SELECT 'FROM ghost_table", expected: [] },
];

for (const { sql, expected } of cases) {
assert.equal(stripSqlCommentsAndLiterals(sql).length, sql.length, sql);
assert.deepEqual(sqlTableAccesses(sql), expected, sql);
}

const entirelyIgnored = "'x'--y\n/*z*/";
assert.equal(stripSqlCommentsAndLiterals(entirelyIgnored), " ".repeat(entirelyIgnored.length));

const quotedIdentifiers = 'SELECT * FROM "orders--archive" JOIN "items/*current*/"';
assert.equal(stripSqlCommentsAndLiterals(quotedIdentifiers), quotedIdentifiers);
});