From aefb6bf0ef0e132c056ae7aefdafb31e6bb47056 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 7 May 2026 19:07:41 +0800 Subject: [PATCH] fix: prevent SELECT with REPLACE/TRUNCATE functions from triggering danger warning The DANGER_RE regex matched keywords anywhere in the SQL text, causing SELECT queries using REPLACE() or TRUNCATE() functions to incorrectly trigger the destructive-SQL warning dialog. Check only the first keyword of each semicolon-separated statement instead. Also fix dialog content overflow by adding min-w-0 to the
element,
and move macOS frameworks config from tauri.conf.json to CI-only
TAURI_CONFIG so local `tauri dev` no longer requires libltdl/libodbc
to be globally installed.
Closes #135
---
.github/workflows/release.yml | 1 +
src-tauri/tauri.conf.json | 3 -
src/components/editor/DangerConfirmDialog.vue | 2 +-
src/composables/useSqlExecution.ts | 5 +-
tests/useSqlExecution.test.ts | 68 +++++++++++++++++++
5 files changed, 73 insertions(+), 6 deletions(-)
create mode 100644 tests/useSqlExecution.test.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b89607f6a9..59113aceed 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -113,6 +113,7 @@ jobs:
APPLE_ID: ${{ startsWith(matrix.platform, 'macos') && secrets.APPLE_ID || '' }}
APPLE_PASSWORD: ${{ startsWith(matrix.platform, 'macos') && secrets.APPLE_PASSWORD || '' }}
APPLE_TEAM_ID: ${{ startsWith(matrix.platform, 'macos') && secrets.APPLE_TEAM_ID || '' }}
+ TAURI_CONFIG: ${{ startsWith(matrix.platform, 'macos') && '{"bundle":{"macOS":{"frameworks":["libodbc.2.dylib","libltdl.7.dylib"]}}}' || '{}' }}
with:
tagName: ${{ github.ref_name }}
releaseName: 'DBX ${{ github.ref_name }}'
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index fb499af4cb..811ac26762 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -29,9 +29,6 @@
"active": true,
"targets": "all",
"createUpdaterArtifacts": true,
- "macOS": {
- "frameworks": ["libodbc.2.dylib", "libltdl.7.dylib"]
- },
"icon": [
"icons/32x32.png",
"icons/128x128.png",
diff --git a/src/components/editor/DangerConfirmDialog.vue b/src/components/editor/DangerConfirmDialog.vue
index b71c04aa98..af9f490bfa 100644
--- a/src/components/editor/DangerConfirmDialog.vue
+++ b/src/components/editor/DangerConfirmDialog.vue
@@ -49,7 +49,7 @@ function onConfirm() {
{{ message || t("dangerDialog.message") }}
{{ details || sql }}
diff --git a/src/composables/useSqlExecution.ts b/src/composables/useSqlExecution.ts
index f8ef088019..7438d2b5ee 100644
--- a/src/composables/useSqlExecution.ts
+++ b/src/composables/useSqlExecution.ts
@@ -6,7 +6,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import type { ConnectionConfig, QueryTab } from "@/types/database";
-const DANGER_RE = /\b(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
+const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i;
export function stripSqlComments(sql: string): string {
return sql
@@ -16,7 +16,8 @@ export function stripSqlComments(sql: string): string {
}
export function isDangerousSql(sql: string): boolean {
- return DANGER_RE.test(stripSqlComments(sql));
+ const cleaned = stripSqlComments(sql);
+ return cleaned.split(";").some((stmt) => DANGER_RE.test(stmt));
}
export function useSqlExecution(deps: {
diff --git a/tests/useSqlExecution.test.ts b/tests/useSqlExecution.test.ts
new file mode 100644
index 0000000000..602c4d5f65
--- /dev/null
+++ b/tests/useSqlExecution.test.ts
@@ -0,0 +1,68 @@
+import { strict as assert } from "node:assert";
+import test from "node:test";
+import { isDangerousSql, stripSqlComments } from "../src/composables/useSqlExecution.ts";
+
+test("stripSqlComments removes block comments", () => {
+ assert.equal(stripSqlComments("SELECT /* drop */ 1").includes("drop"), false);
+});
+
+test("stripSqlComments removes line comments", () => {
+ assert.equal(stripSqlComments("SELECT 1 -- DROP TABLE").includes("DROP"), false);
+});
+
+test("stripSqlComments removes MySQL hash comments", () => {
+ assert.equal(stripSqlComments("SELECT 1 # DELETE").includes("DELETE"), false);
+});
+
+test("SELECT with REPLACE function is not dangerous", () => {
+ assert.equal(isDangerousSql("SELECT REPLACE(col, ';', ',') FROM t"), false);
+});
+
+test("SELECT with TRUNCATE function is not dangerous", () => {
+ assert.equal(isDangerousSql("SELECT TRUNCATE(1.234, 2)"), false);
+});
+
+test("REPLACE INTO is dangerous", () => {
+ assert.equal(isDangerousSql("REPLACE INTO t VALUES (1, 2)"), true);
+});
+
+test("DELETE FROM is dangerous", () => {
+ assert.equal(isDangerousSql("DELETE FROM t WHERE id = 1"), true);
+});
+
+test("DROP TABLE is dangerous", () => {
+ assert.equal(isDangerousSql("DROP TABLE t"), true);
+});
+
+test("UPDATE is dangerous", () => {
+ assert.equal(isDangerousSql("UPDATE t SET col = 1"), true);
+});
+
+test("ALTER TABLE is dangerous", () => {
+ assert.equal(isDangerousSql("ALTER TABLE t ADD COLUMN c INT"), true);
+});
+
+test("TRUNCATE TABLE is dangerous", () => {
+ assert.equal(isDangerousSql("TRUNCATE TABLE t"), true);
+});
+
+test("multi-statement with danger in second statement is dangerous", () => {
+ assert.equal(isDangerousSql("SELECT 1; DROP TABLE t"), true);
+});
+
+test("danger keyword inside comment is not dangerous", () => {
+ assert.equal(isDangerousSql("SELECT 1 /* DROP TABLE t */"), false);
+});
+
+test("leading whitespace before danger keyword is dangerous", () => {
+ assert.equal(isDangerousSql(" DELETE FROM t"), true);
+});
+
+test("complex SELECT with joins and functions is not dangerous", () => {
+ const sql = `select mkey, replace(xxxx,';',',') as xxxx_content,
+ SUBSTRING_INDEX(SUBSTRING_INDEX(pmid,'/',3),'/',-1) as xxx
+ from m_xxx left join (
+ select xxx from m_alarm where alarm_time >= SUBDATE(now(), interval 3 minute)
+ ) as xxxx on m_alarm.mid = m_alarm_tmp.mid`;
+ assert.equal(isDangerousSql(sql), false);
+});