* Execute a query inside a transaction that always rolls back, preventing * any modifications from persisting. SQL Server has no native READ ONLY * transaction mode, so this is the defense-in-depth backstop behind the * keyword classifier. * * Because the rollback guard is application
(
processedSQL: string,
parameters?: any[],
)
| 716 | * COMMIT/ROLLBACK inside string literals that stripCommentsAndStrings removes |
| 717 | */ |
| 718 | private async executeReadOnly( |
| 719 | processedSQL: string, |
| 720 | parameters?: any[], |
| 721 | ): Promise<SQLResult> { |
| 722 | const cleaned = stripCommentsAndStrings(processedSQL, "sqlserver").toLowerCase(); |
| 723 | if (/\b(?:commit|rollback)\b/.test(cleaned)) { |
| 724 | throw new Error( |
| 725 | "Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed", |
| 726 | ); |
| 727 | } |
| 728 | if (/\b(?:exec|execute|sp_executesql|xp_cmdshell)\b/.test(cleaned)) { |
| 729 | throw new Error( |
| 730 | "Read-only mode: dynamic SQL execution (EXEC, EXECUTE, sp_executesql, xp_cmdshell) is not allowed", |
| 731 | ); |
| 732 | } |
| 733 | |
| 734 | const transaction = new sql.Transaction(this.connection!); |
| 735 | await transaction.begin(); |
| 736 | |
| 737 | const request = new sql.Request(transaction); |
| 738 | const messages: DatabaseMessage[] = []; |
| 739 | request.on( |
| 740 | 'info', |
| 741 | (info: { message: string; number?: number; class?: number; lineNumber?: number }) => { |
| 742 | messages.push({ |
| 743 | text: info.message, |
| 744 | severity: info.class !== undefined ? String(info.class) : undefined, |
| 745 | code: info.number, |
| 746 | line: info.lineNumber, |
| 747 | }); |
| 748 | }, |
| 749 | ); |
| 750 | |
| 751 | if (parameters && parameters.length > 0) { |
| 752 | parameters.forEach((param, index) => { |
| 753 | const paramName = `p${index + 1}`; |
| 754 | if (typeof param === 'string') { |
| 755 | request.input(paramName, sql.VarChar, param); |
| 756 | } else if (typeof param === 'number') { |
| 757 | if (Number.isInteger(param)) { |
| 758 | request.input(paramName, sql.Int, param); |
| 759 | } else { |
| 760 | request.input(paramName, sql.Float, param); |
| 761 | } |
| 762 | } else if (typeof param === 'boolean') { |
| 763 | request.input(paramName, sql.Bit, param); |
| 764 | } else if (param === null || param === undefined) { |
| 765 | request.input(paramName, sql.VarChar, param); |
| 766 | } else if (Array.isArray(param)) { |
| 767 | request.input(paramName, sql.VarChar, JSON.stringify(param)); |
| 768 | } else { |
| 769 | request.input(paramName, sql.VarChar, JSON.stringify(param)); |
| 770 | } |
| 771 | }); |
| 772 | } |
| 773 | |
| 774 | let result; |
| 775 | let queryFailed = false; |
no test coverage detected