mssql: the query runs as a read-only login, one line, no variables; sqlcmd is installed (hq #193) #210

Merged
mesh-admin merged 1 commits from fix/193-mssql-reads-as-a-reader into main 2026-10-01 22:30:03 +00:00
Contributor

The companion to #209, for the same class of hole in mssql_query (hq issue 193).

Live today, mssql's tools are dead. The runtime image never installed sqlcmd, so mssql_list_databases fails with spawn sqlcmd ENOENT. The holes below were therefore not reachable live, and this PR makes the tools work for the first time. That is why both changes go in one PR.

What was proven on a throwaway SQL Server 2022 (default bridge, no published port), with sqlcmd run the way the old code ran it:

  • SELECT '$(SQLCMDPASSWORD)' returned the sa password. sqlcmd substitutes $(NAME) from the environment, and the administrator's password is in it.
  • A line beginning :!! echo … ran a program in the tools container, which holds the sa and bus credentials.
  • go-sqlcmd v1.10's -X (disable commands) makes it ignore -Q and go interactive, so that flag cannot be the guard.

The fix:

  • The statement runs as mesh_mssql_reader, with CONNECT ANY DATABASE and SELECT ALL USER SECURABLES and nothing else. Every start takes it out of sysadmin and re-sets its password. The password is a new own-secret, reader. Without it, the call is refused and never falls back to sa.
  • Caller text runs with -x (no variable substitution). It follows the module's own text on the first line, and any CR or LF is refused before sqlcmd starts. sqlcmd only treats :/!! as commands at the start of a line: tried mid-line, after a lone CR, and indented, each was a syntax error.
  • No BEGIN TRANSACTION … ROLLBACK text. The old wrapper also double-wrapped FOR JSON.
  • go-sqlcmd v1.10.0 is installed at a pinned sha256. The stage was built on its own and runs.

Proof with the fix, same throwaway server:

  • SELECT returns rows, and $(SQLCMDPASSWORD) comes back literally.
  • Refused: COMMIT; DROP, DELETE, xp_cmdshell, EXECUTE AS LOGIN = 'sa', adding itself to sysadmin, CREATE TABLE, and a line-break :!! (no file created).
  • IS_SRVROLEMEMBER('sysadmin') = 0, as mesh_mssql_reader.

Tests. test/reader.test.ts (4, fake sqlcmd). Added build/test scripts to package.json.

Rollout. After build, the next push mints reader on mssql's node and recreates mssql's runtime container. The server container is untouched. Verify with mssql_list_databases (works at all) and mssql_query → SELECT SUSER_NAME() = mesh_mssql_reader.

The companion to #209, for the same class of hole in `mssql_query` (hq issue 193). **Live today, mssql's tools are dead.** The runtime image never installed `sqlcmd`, so `mssql_list_databases` fails with `spawn sqlcmd ENOENT`. The holes below were therefore not reachable live, and this PR makes the tools work for the first time. That is why both changes go in one PR. **What was proven on a throwaway SQL Server 2022 (default bridge, no published port), with sqlcmd run the way the old code ran it:** - `SELECT '$(SQLCMDPASSWORD)'` returned **the sa password**. sqlcmd substitutes `$(NAME)` from the environment, and the administrator's password is in it. - A line beginning `:!! echo …` **ran a program in the tools container**, which holds the sa and bus credentials. - go-sqlcmd v1.10's `-X` (disable commands) makes it ignore `-Q` and go interactive, so that flag cannot be the guard. **The fix:** - The statement runs as `mesh_mssql_reader`, with `CONNECT ANY DATABASE` and `SELECT ALL USER SECURABLES` and nothing else. Every start takes it out of sysadmin and re-sets its password. The password is a new own-secret, `reader`. Without it, the call is refused and never falls back to sa. - Caller text runs with `-x` (no variable substitution). It follows the module's own text on the first line, and any CR or LF is refused before sqlcmd starts. sqlcmd only treats `:`/`!!` as commands at the start of a line: tried mid-line, after a lone CR, and indented, each was a syntax error. - No `BEGIN TRANSACTION … ROLLBACK` text. The old wrapper also double-wrapped `FOR JSON`. - go-sqlcmd v1.10.0 is installed at a pinned sha256. The stage was built on its own and runs. **Proof with the fix, same throwaway server:** - `SELECT` returns rows, and `$(SQLCMDPASSWORD)` comes back literally. - Refused: `COMMIT; DROP`, `DELETE`, `xp_cmdshell`, `EXECUTE AS LOGIN = 'sa'`, adding itself to sysadmin, `CREATE TABLE`, and a line-break `:!!` (no file created). - `IS_SRVROLEMEMBER('sysadmin')` = 0, as `mesh_mssql_reader`. **Tests.** `test/reader.test.ts` (4, fake sqlcmd). Added `build`/`test` scripts to package.json. **Rollout.** After build, the next push mints `reader` on mssql's node and recreates mssql's runtime container. The server container is untouched. Verify with `mssql_list_databases` (works at all) and `mssql_query` → `SELECT SUSER_NAME()` = `mesh_mssql_reader`.
mesh-admin added 1 commit 2026-10-01 22:25:36 +00:00
Proven on a throwaway server: as the administrator a caller's $(SQLCMDPASSWORD) returned
the sa password, and a line beginning ':!!' ran a program in the tools container. The
statement now runs as mesh_mssql_reader (CONNECT ANY DATABASE, SELECT ALL USER SECURABLES),
with substitution off (-x), after the module's own text on the first line, and a line break
is refused. go-sqlcmd v1.10.0 is installed at a pinned digest: the image never had sqlcmd,
so every mssql tool failed with spawn sqlcmd ENOENT.
mesh-admin merged commit fec6d76fb3 into main 2026-10-01 22:30:03 +00:00
mesh-admin deleted branch fix/193-mssql-reads-as-a-reader 2026-10-01 22:30:03 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#210