UNPKG

@bytebase/dbhub

Version:

Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite

183 lines (180 loc) • 5.61 kB
import { stripCommentsAndStrings } from "./chunk-IXC4NQGC.js"; // src/utils/allowed-keywords.ts var allowedKeywords = { postgres: ["select", "with", "explain", "show"], mysql: ["select", "with", "explain", "show", "describe", "desc"], mariadb: ["select", "with", "explain", "show", "describe", "desc"], sqlite: ["select", "with", "explain", "pragma"], // SQL Server has no native EXPLAIN statement; the connector translates a // leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector). sqlserver: ["select", "with", "explain"], // Oracle's native form is `EXPLAIN PLAN FOR <stmt>`; the connector accepts // both that and a bare `EXPLAIN <stmt>` and reads the plan back through // DBMS_XPLAN (see OracleConnector.explainQuery). EXPLAIN PLAN only parses // the statement, it never executes it. oracle: ["select", "with", "explain"] }; var mutatingKeywords = [ "insert", "update", "delete", "drop", "alter", "create", "truncate", "merge", "grant", "revoke", "rename" ]; var mutatingPattern = new RegExp( `\\b(?:${mutatingKeywords.join("|")})\\b`, "i" ); var mutatingPatternWithReplace = new RegExp( `\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`, "i" ); var sqlServerDynamicSqlKeywords = [ "execute", "exec", "sp_executesql", "xp_cmdshell" ]; var sqlServerDynamicSqlPattern = new RegExp( `\\b(?:${sqlServerDynamicSqlKeywords.join("|")})\\b`, "i" ); var sqlServerPassThroughKeywords = [ "openquery", "openrowset", "opendatasource" ]; var sqlServerPassThroughPattern = new RegExp( `\\b(?:${sqlServerPassThroughKeywords.join("|")})\\s*\\(`, "i" ); var escapeHatchFunctionKeywords = { mysql: ["load_file", "get_lock", "release_lock", "release_all_locks"], mariadb: ["load_file", "get_lock", "release_lock", "release_all_locks"], postgres: ["pg_read_file", "pg_read_binary_file", "pg_ls_dir", "set_config"], sqlserver: sqlServerPassThroughKeywords, // Oracle packages a SELECT can call to reach outside the database, matched // as `package.member(` (see escapeHatchCallSuffix). oracle: [ "utl_http", "utl_tcp", "utl_smtp", "utl_inaddr", "utl_file", "dbms_ldap", "dbms_sql", "dbms_scheduler", "dbms_java", "dbms_lob", "dbms_xslprocessor", "dbms_advisor" ] }; var escapeHatchCallSuffix = { oracle: "\\s*\\.\\s*[a-z_][a-z0-9_$#]*\\s*\\(" }; var DEFAULT_CALL_SUFFIX = "\\s*\\("; var escapeHatchFunctionPatterns = Object.fromEntries( Object.entries(escapeHatchFunctionKeywords).map(([type, keywords]) => [ type, new RegExp( `\\b(?:${keywords.join("|")})${escapeHatchCallSuffix[type] ?? DEFAULT_CALL_SUFFIX}`, "i" ) ]) ); function hasEscapeHatchFunction(strippedSQL, connectorType) { const pattern = escapeHatchFunctionPatterns[connectorType]; return pattern !== void 0 && pattern.test(strippedSQL); } var mutatingPatternSqlServer = new RegExp( `\\b(?:${[...mutatingKeywords, ...sqlServerDynamicSqlKeywords].join("|")})\\b`, "i" ); var mutatingPatterns = { postgres: mutatingPattern, mysql: mutatingPatternWithReplace, mariadb: mutatingPatternWithReplace, sqlite: mutatingPatternWithReplace, sqlserver: mutatingPatternSqlServer, oracle: mutatingPattern }; function hasMutatingKeyword(strippedSQL, connectorType) { return (mutatingPatterns[connectorType] ?? mutatingPattern).test(strippedSQL); } var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i; var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([ "table_info", "index_info", "index_list", "foreign_key_list" ]); var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/; var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i; function isReadOnlySQL(sql, connectorType) { return checkReadOnly( stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(), connectorType ); } function getFirstKeyword(sql, connectorType) { const cleaned = stripCommentsAndStrings(sql, connectorType).trim(); return cleaned.match(/\S+/)?.[0]?.toLowerCase() ?? ""; } function checkReadOnly(cleanedSQL, connectorType) { if (!cleanedSQL) { return false; } const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? ""; const keywordList = allowedKeywords[connectorType] || []; if (!keywordList.includes(firstWord)) { return false; } if (hasEscapeHatchFunction(cleanedSQL, connectorType)) { return false; } if (firstWord === "with" && hasMutatingKeyword(cleanedSQL, connectorType)) { return false; } if (firstWord === "pragma" && connectorType === "sqlite") { if (cleanedSQL.includes("=")) { return false; } const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern); if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) { return false; } } if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) { return false; } if (firstWord === "explain") { const m = explainAnalyzePattern.exec(cleanedSQL); if (m) { const afterExplain = cleanedSQL.slice(m[0].length).trim(); if (afterExplain && !checkReadOnly(afterExplain, connectorType)) { return false; } } } return true; } export { allowedKeywords, sqlServerDynamicSqlKeywords, sqlServerDynamicSqlPattern, sqlServerPassThroughKeywords, sqlServerPassThroughPattern, hasEscapeHatchFunction, hasMutatingKeyword, isReadOnlySQL, getFirstKeyword };