BACK TO SEARCH
MikageSawatari/clickhouse-mcp-servercritical
A remote MCP server for Claude to interact with ClickHouse databases
This MCP server provides a secure, read-only interface for Claude to interact with ClickHouse databases. It authenticates users via Google OAuth 2.0 w...
purpose: This MCP server provides a secure, read-only interthreat: network exposed
RISK SCORE
0/ 100 risk
low findings+5
high findings+50
medium findings+45
capped at100
VULNERABILITY ANALYSIS · 6 findings in 6 blocks2 HIGH · 3 MEDIUM
HIGH1 finding
src/server/mcp-handler.ts:204
204private async executeQuery(args: any) {
205 await this.initPromise;
206 const { query, format = 'JSONCompact' } = args;
207 ...
208 if (!query.trim().toUpperCase().startsWith('SELECT')) {
209 throw new Error('読み取り専用モードです。SELECTクエリのみ実行できます。');
210 }
211 try {
212 const queryResult = await this.clickhouseClient.query({
213 query,
214 format: 'JSON'
215 });
216 ...
217 }src/index.ts:89→src/server/mcp-handler.ts:1-10
// Exploitable by any authenticated user via the MCP interface. Since the MCP is network-exposed, an attacker who obtains a valid token can exploit this.
EXPLAINThe 'query' tool accepts arbitrary SQL query strings from the user and passes them directly to ClickHouse. The only validation is a check that the query starts with 'SELECT', but this can be bypassed using comments, newlines, or other SQL constructs (e.g., 'SELECT 1; DROP TABLE users;'). This allows an attacker to execute arbitrary SQL commands, including destructive operations, despite the 'readonly' setting in the ClickHouse client configuration.
IMPACTAn attacker could execute arbitrary SQL queries, potentially reading, modifying, or deleting data in the ClickHouse database. The 'readonly' setting may mitigate some write operations, but it is not a security boundary and can be bypassed with certain ClickHouse features.
FIXUse parameterized queries or a query builder to separate SQL code from user input. Validate that the query contains only allowed operations (e.g., SELECT) and reject any other statements. Consider using a whitelist of allowed query patterns.
HIGH1 finding
src/server/mcp-handler.ts:694
694private async getSampleData(args: any) {
695 const { table, limit = 10 } = args;
696 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
697 throw new Error('無効なテーブル名です');
698 }
699 const query = `SELECT * FROM ${table} LIMIT ${Math.min(Math.max(1, limit), 1000)}`;
700 const result = await this.clickhouseClient.query({ query });
701 ...
702}src/index.ts:89→src/server/mcp-handler.ts:1-10
// Exploitable by any authenticated user via the MCP interface.
EXPLAINThe 'sample_data' tool constructs a SQL query by directly interpolating the 'table' parameter into the query string. Although there is a regex validation for the table name, the 'limit' parameter is also directly interpolated after a numeric clamp. The regex for table name is restrictive, but the limit parameter is still injected unsafely. However, the primary risk is that the table name validation could be bypassed if the regex is not comprehensive (e.g., backticks or other escaping). Additionally, the limit parameter is not parameterized, allowing potential injection if the clamp is bypassed.
IMPACTAn attacker could potentially inject SQL through the limit parameter or bypass the table name validation to execute arbitrary queries.
FIXUse parameterized queries for both table and limit parameters. Avoid string interpolation entirely.
MEDIUM1 finding
src/server/mcp-handler.ts:204
204private async executeQuery(args: any) {
205 ...
206 if (!query.trim().toUpperCase().startsWith('SELECT')) {
207 throw new Error('読み取り専用モードです。SELECTクエリのみ実行できます。');
208 }
209 ...
210 const queryResult = await this.clickhouseClient.query({
211 query,
212 format: 'JSON'
213 });
214 ...
215}src/index.ts:89→src/server/mcp-handler.ts:1-10
// Exploitable by any authenticated user via the MCP interface.
EXPLAINThe 'query' tool allows any SELECT query to be executed, including queries that access system tables, other databases, or perform information_schema queries that may expose sensitive metadata or data beyond the intended scope of the application. The tool does not restrict which tables or databases can be queried, allowing an attacker to enumerate the entire ClickHouse instance.
IMPACTAn attacker could query system tables to extract credentials, user information, or other sensitive data stored in ClickHouse. They could also access any database or table accessible to the configured ClickHouse user.
FIXImplement a whitelist of allowed tables or databases, or restrict queries to the current database only. Use a query parser to validate that only safe operations are performed.
MEDIUM1 finding
src/index.ts:543
543app.get('/mcp-clickhouse/oauth/authorize', (req, res) => {
544 const { client_id, redirect_uri, state } = req.query;
545 const sessionState = crypto.randomBytes(16).toString('hex');
546 authorizedTokens.add(`session:${sessionState}:${JSON.stringify({
547 client_id, redirect_uri, state
548 })}`);
549 ...
550});src/index.ts:1-10
// Exploitable by any network attacker, as the MCP is network-exposed.
EXPLAINThe OAuth authorization endpoint accepts a redirect_uri parameter without validating it against a whitelist of allowed URIs. This could allow an attacker to use the server as an open redirector, or to steal authorization codes by redirecting to a malicious site.
IMPACTAn attacker could craft a link that redirects users to a malicious site after authorization, potentially stealing the authorization code and gaining access to the ClickHouse database.
FIXValidate the redirect_uri against a whitelist of allowed URIs. Reject any request with an unrecognized redirect_uri.
MEDIUM1 finding
src/index.ts:543
543app.get('/mcp-clickhouse/oauth/authorize', (req, res) => {
544 const { client_id, redirect_uri, state } = req.query;
545 const sessionState = crypto.randomBytes(16).toString('hex');
546 authorizedTokens.add(`session:${sessionState}:${JSON.stringify({
547 client_id, redirect_uri, state
548 })}`);
549 const googleAuthUrl = googleOAuthClient.generateAuthUrl({
550 access_type: 'online',
551 scope: ['email', 'profile'],
552 state: sessionState
553 });
554 res.redirect(googleAuthUrl);
555});src/index.ts:1-10
// Exploitable by any network attacker, as the MCP is network-exposed.
EXPLAINThe OAuth authorization endpoint does not implement any rate limiting or brute-force protection. An attacker could repeatedly request authorization codes, potentially leading to resource exhaustion or abuse of the Google OAuth flow.
IMPACTAn attacker could perform a denial-of-service attack by flooding the authorization endpoint, or potentially brute-force the state parameter to hijack sessions.
FIXImplement rate limiting on authentication endpoints using a library like express-rate-limit. Also consider adding CAPTCHA or other anti-automation measures.
LOW1 finding
src/index.ts:66
66const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');src/index.ts:1-10
// Exploitable only if the attacker can predict the random seed, which is unlikely.
EXPLAINThe JWT secret is generated using crypto.randomBytes, which is cryptographically secure. However, if the environment variable is not set, the secret is generated at startup and is not persisted. This means that if the server restarts, all existing tokens become invalid. More importantly, if an attacker can predict the random value (e.g., by knowing the server start time), they could forge tokens. However, crypto.randomBytes is generally secure.
IMPACTLow risk, but if the random number generator is seeded predictably, an attacker could forge JWT tokens.
FIXAlways set JWT_SECRET via environment variable. Use a strong, persistent secret.
◷ 5/22/2026
Findings are produced by automated LLM analysis and may include false positives or miss issues. Verify independently before acting.