Parsing/Connection string handling
Description
The commit fixes MSSQL connection string handling when credentials contain special characters (notably semicolons and closing braces). Previously, username and password values that include ';' or '}' could break the connection string parsing by the go-mssqldb driver because ';' is a delimiter and braces are not escaped. The patch introduces odbc escaping: when either the username or password contains special characters, both values are wrapped in ODBC braces and the connection string is prefixed with 'odbc:'. It also normalizes and guards the odbc: prefix to prevent double prefixes and handles the Kerberos path accordingly. This hardens credential handling in the DSN and ensures correct parsing by the driver, preventing authentication failures due to misparsed credentials.
Proof of Concept
Proof of concept:
Example credentials: user 'myuser', password 'P@ss;word1', host 'localhost', database 'database'.
Pre-fix (older Grafana, e.g. 12.3.x): Grafana would build a connection string like:
server=localhost;database=database;user id=myuser;password=P@ss;word1;
This causes the MSSQL driver to treat ';' as a delimiter, truncating the password and leading to authentication failure or misparsing of subsequent tokens.
Post-fix (with this commit, 12.4.x): The code escapes values and prefixes with 'odbc:', producing:
odbc:server=localhost;database=database;user id={myuser};password={P@ss;word1};
This ensures the password and username are passed as a single value to the driver.
Repro steps:
1) Configure an MSSQL data source with user 'myuser' and password 'P@ss;word1' on localhost/database.
2) Run Grafana 12.3.x: attempt to connect; expect failure due to credential parsing.
3) Upgrade to Grafana 12.4.0 with this patch and attempt to connect again; authentication should succeed.
Commit Details
Author: MdTanwer
Date: 2026-03-17 18:36 UTC
Message:
MSSQL: Fix connection when password contains semicolon (#118076)
* MSSQL: escape user/password in connection string when they contain ; or }
Passwords (and usernames) with semicolons or closing braces broke connection
string parsing. When either value needs escaping, we now wrap them in ODBC
braces and use the odbc: prefix so the go-mssqldb driver parses them
correctly.
* Update pkg/tsdb/mssql/sqleng/connection.go
Co-authored-by: Adam Yeats <16296989+adamyeats@users.noreply.github.com>
* mssql: guard against double odbc: prefix in connection string
Normalise connStr with strings.TrimPrefix(connStr, "odbc:") before
adding the prefix when ODBC escaping is used, so future refactors or
other auth types cannot produce odbc:odbc:...
* mssql: apply odbc prefix once in Kerberos path
Compute useOdbc once from user/pass; call Krb5ParseAuthCredentials
once (raw DSN) and apply odbc: prefix only when useOdbc, so the
Kerberos path does not depend on helper internals.
* mssql: fix goimports formatting (tabs in odbcNeedsEscape)
---------
Co-authored-by: Adam Yeats <16296989+adamyeats@users.noreply.github.com>
Triage Assessment
Vulnerability Type: Parsing/Connection string handling
Confidence: MEDIUM
Reasoning:
The commit adds escaping for user/password values in MSSQL connection strings when special characters like semicolons or closing braces are present. This prevents malformed connection string parsing by the go-mssqldb driver, which could otherwise lead to authentication failures or potential information exposure due to incorrect parsing. It effectively hardens input handling for credentials in the connection string.
Verification Assessment
Vulnerability Type: Parsing/Connection string handling
Confidence: MEDIUM
Affected Versions: < 12.4.0
Code Diff
diff --git a/pkg/tsdb/mssql/sqleng/connection.go b/pkg/tsdb/mssql/sqleng/connection.go
index 7b779b547c817..dff0d65346904 100644
--- a/pkg/tsdb/mssql/sqleng/connection.go
+++ b/pkg/tsdb/mssql/sqleng/connection.go
@@ -3,6 +3,7 @@ package sqleng
import (
"database/sql"
"fmt"
+ "strings"
"time"
"github.com/grafana/grafana-azure-sdk-go/v2/azcredentials"
@@ -17,6 +18,20 @@ import (
"github.com/microsoft/go-mssqldb/azuread"
)
+// odbcNeedsEscape returns true if the value contains semicolon or closing brace,
+// which would break connection string parsing (semicolon is the key=value delimiter).
+func odbcNeedsEscape(s string) bool {
+ return strings.ContainsAny(s, ";}") || s != strings.TrimSpace(s)
+}
+
+// escapeOdbcValue wraps a connection string value in ODBC braces so that semicolons
+// and other special characters (e.g. in passwords) are not interpreted as delimiters.
+// The go-mssqldb driver uses this when the connection string has the "odbc:" prefix.
+func escapeOdbcValue(s string) string {
+ escaped := strings.ReplaceAll(s, "}", "}}")
+ return "{" + escaped + "}"
+}
+
func newMSSQL(driverName string, rowLimit int64, dsInfo DataSourceInfo, cnnstr string, logger log.Logger, proxyClient proxy.Client) (*sql.DB, error) {
var connector *mssql.Connector
var err error
@@ -121,9 +136,28 @@ func generateConnectionString(dsInfo DataSourceInfo, azureCredentials azcredenti
case windowsAuthentication:
// No user id or password. We're using windows single sign on.
case kerberosRaw, kerberosKeytab, kerberosCredentialCacheFile, kerberosCredentialCache:
- connStr = kerberos.Krb5ParseAuthCredentials(addr.Host, addr.Port, dsInfo.Database, dsInfo.User, dsInfo.DecryptedSecureJSONData["password"], kerberosAuth)
+ user := dsInfo.User
+ pass := dsInfo.DecryptedSecureJSONData["password"]
+ useOdbc := odbcNeedsEscape(pass) || odbcNeedsEscape(user)
+ if useOdbc {
+ user = escapeOdbcValue(user)
+ pass = escapeOdbcValue(pass)
+ }
+
+ connStr = kerberos.Krb5ParseAuthCredentials(addr.Host, addr.Port, dsInfo.Database, user, pass, kerberosAuth)
+ if useOdbc {
+ connStr = "odbc:" + strings.TrimPrefix(connStr, "odbc:")
+ }
default:
- connStr += fmt.Sprintf("user id=%s;password=%s;", dsInfo.User, dsInfo.DecryptedSecureJSONData["password"])
+ user := dsInfo.User
+ pass := dsInfo.DecryptedSecureJSONData["password"]
+ if odbcNeedsEscape(pass) || odbcNeedsEscape(user) {
+ user = escapeOdbcValue(user)
+ pass = escapeOdbcValue(pass)
+ connStr = "odbc:" + strings.TrimPrefix(connStr, "odbc:") + fmt.Sprintf("user id=%s;password=%s;", user, pass)
+ } else {
+ connStr += fmt.Sprintf("user id=%s;password=%s;", user, pass)
+ }
}
// Port number 0 means to determine the port automatically, so we can let the driver choose
diff --git a/pkg/tsdb/mssql/sqleng/sql_engine_test.go b/pkg/tsdb/mssql/sqleng/sql_engine_test.go
index 8a354fadc2fe8..6453021a3cf03 100644
--- a/pkg/tsdb/mssql/sqleng/sql_engine_test.go
+++ b/pkg/tsdb/mssql/sqleng/sql_engine_test.go
@@ -691,6 +691,19 @@ func TestGenerateConnectionString(t *testing.T) {
},
expConnStr: "server=localhost;database=database;user id=user;password=;",
},
+ {
+ desc: "Password with semicolon (ODBC escaping)",
+ dataSource: DataSourceInfo{
+ URL: "localhost",
+ Database: "database",
+ User: "myuser",
+ DecryptedSecureJSONData: map[string]string{
+ "password": "StrongPass;123",
+ },
+ JsonData: JsonData{},
+ },
+ expConnStr: "odbc:server=localhost;database=database;user id={myuser};password={StrongPass;123};",
+ },
}
logger := backend.NewLoggerWith("logger", "mssql.test")