fix query route safe clickhouse error codes (#1268)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Refined analytics query error classification so certain database
errors are treated as known/handled, reducing exposure of internal
diagnostics in responses.

* **Tests**
* Added end-to-end tests verifying safe (masked) error responses,
preventing leakage of restricted column/identifier details and
constraining suggestion text in error messages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
BilalG1 2026-03-23 10:30:00 -07:00 committed by GitHub
parent d51c303fb0
commit 381e057c1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 0 deletions

View File

@ -83,6 +83,8 @@ const SAFE_CLICKHOUSE_ERROR_CODES = [
const UNSAFE_CLICKHOUSE_ERROR_CODES = [
36, // BAD_ARGUMENTS
43, // ILLEGAL_TYPE_OF_ARGUMENT
47, // UNKNOWN_IDENTIFIER
60, // UNKNOWN_TABLE
497, // ACCESS_DENIED
];

View File

@ -1524,6 +1524,103 @@ it("does not allow input() function", async ({ expect }) => {
`);
});
it("returns a safe error for illegal type of argument (code 43)", async ({ expect }) => {
const response = await runQuery({
query: "SELECT arrayJoin(123)",
});
expect(stripQueryId(response, expect)).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "ANALYTICS_QUERY_ERROR",
"details": {
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 43 Argument for function arrayJoin must be Array or Map: In scope SELECT arrayJoin(123).
\`,
},
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 43 Argument for function arrayJoin must be Array or Map: In scope SELECT arrayJoin(123).
\`,
},
"headers": Headers {
"x-stack-known-error": "ANALYTICS_QUERY_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("does not leak column names from restricted tables via illegal type of argument (code 43)", async ({ expect }) => {
// ClickHouse resolves identifiers and checks types before checking permissions,
// so a code 43 error referencing a restricted table column could leak its name/type
const response = await runQuery({
query: "SELECT arrayJoin(query) FROM system.query_log",
});
expect(stripQueryId(response, expect)).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "ANALYTICS_QUERY_ERROR",
"details": {
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 43 Argument for function arrayJoin must be Array or Map: In scope SELECT arrayJoin(query) FROM system.query_log.
\`,
},
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 43 Argument for function arrayJoin must be Array or Map: In scope SELECT arrayJoin(query) FROM system.query_log.
\`,
},
"headers": Headers {
"x-stack-known-error": "ANALYTICS_QUERY_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("does not leak column names from restricted tables via unknown identifier (code 47)", async ({ expect }) => {
// ClickHouse resolves identifiers before checking permissions, and suggests
// real column names ("Maybe you meant: ..."), so code 47 must be unsafe
const response = await runQuery({
query: "SELECT qurey FROM system.query_log",
});
expect(stripQueryId(response, expect)).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "ANALYTICS_QUERY_ERROR",
"details": {
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 47 Unknown expression identifier \\\`qurey\\\` in scope SELECT qurey FROM system.query_log. Maybe you meant: ['query'].
\`,
},
"error": deindent\`
Error during execution of this query.
As you are in development mode, you can see the full error: 47 Unknown expression identifier \\\`qurey\\\` in scope SELECT qurey FROM system.query_log. Maybe you meant: ['query'].
\`,
},
"headers": Headers {
"x-stack-known-error": "ANALYTICS_QUERY_ERROR",
<some fields may have been hidden>,
},
}
`);
});
it("does not allow numbers table function with large values", async ({ expect }) => {
const response = await runQuery({
query: "SELECT * FROM numbers(1000000000)",