hasura / hasura/graphql-engine

Console crashes when rendering database object names containing regex metacharacters

Open
#10,840 2 comments 0 reactions 0 assignees View on GitHub
k/bug
Dominant language
TypeScript
Stars
32.1k
Forks
3k
PR merge metrics
PR metrics pending

Description

### Version Information

Server Version: v2.48.16
CLI Version (for CLI related issue): N/A

### Environment

Hasura GraphQL Engine V2 Console
Provider: Docker / Hasura Cloud / local dev setup
Database: PostgreSQL
Browser: Chrome / Chromium

### What is the current behaviour?

The Hasura Console can crash and show the global error page when database object names containing regular expression metacharacters are rendered in the Data sidebar/tree.

I observed this with an object name containing an unbalanced `(`. The Console displayed:

`SyntaxError: Invalid regular expression: /\/data\/default\/schema\/public\/(tables|functions|views)\/.../: Unterminated group`

This prevents normal use of parts of the Data Console until the problematic route/object is avoided.

The likely cause is that database/source/schema/object names are interpolated directly into `RegExp` patterns without escaping:

- `frontend/libs/console/legacy-ce/src/lib/components/Services/Data/TreeView.tsx`
```ts
const regex = new RegExp(
`\\/data\\/${currentSource}\\/schema\\/${currentSchema}\\/(tables|functions|views)\\/${item.name}\\/`
);

### What is the expected behaviour?
The Console should treat database object names and search terms as literal strings. Regex metacharacters such as (, [, +, ?, *, |, \, etc. should not crash rendering or alter matching semantics.

### How to reproduce the issue?
Run Hasura GraphQL Engine V2 with a PostgreSQL source.
Create or track a table/function/view whose name contains a regex metacharacter, for example an unbalanced (.
Open the Hasura Console and navigate to the Data tab / affected schema or object.
The Console renders the Data tree and throws a SyntaxError: Invalid regular expression.
The app falls back to the global Error page, making parts of the Console unusable.

OR
Execute following code with `./poc/hasura_console_regex_crash_poc.py --url http://localhost:18080`

```
#!/usr/bin/env python3
"""
Hasura Console crash via unescaped RegExp input.

This script prepares a local Hasura V2 instance for reproduction and opens the
crashing Console route automatically. No manual Console interaction is needed.

Tested against the repository Docker manifest image:
hasura/graphql-engine:v2.48.16
"""

import argparse
import json
import os
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

DEFAULT_DATABASE_URL = (
"postgres://postgres:postgrespassword@provally-hasura-postgres:5432/postgres"
)

def parse_args():
parser = argparse.ArgumentParser(
description="Trigger the Hasura Console RegExp crash without manual interaction."
)
parser.add_argument(
"--url",
default="http://localhost:18080",
help="Base URL of the Hasura GraphQL Engine server.",
)
parser.add_argument(
"--source",
default="default",
help="Hasura source name to create/use.",
)
parser.add_argument(
"--schema",
default="public",
help="Postgres schema to create/use.",
)
parser.add_argument(
"--table-name",
default="provally_crash_(",
help="Quoted Postgres table name containing a regex metacharacter.",
)
parser.add_argument(
"--database-url",
default=DEFAULT_DATABASE_URL,
help="Database URL as seen from the Hasura container.",
)
parser.add_argument(
"--browser-app",
default="",
help='macOS browser app name, e.g. "Google Chrome". Defaults to system browser.',
)
parser.add_argument(
"--no-open",
action="store_true",
help="Prepare the PoC but do not open the browser.",
)
parser.add_argument(
"--wait-seconds",
type=int,
default=5,
help="Seconds to wait after opening the crash URL.",
)
return parser.parse_args()

def base_url(value):
return value.rstrip("/")

def check_tcp(url, timeout=5):
parsed = urllib.parse.urlparse(url)
host = parsed.hostname or "localhost"
port = parsed.port or (443 if parsed.scheme == "https" else 80)
with socket.create_connection((host, port), timeout=timeout):
return host, port

def request_json(method, url, payload=None, timeout=30):
data = None
headers = {"Content-Type": "application/json"}
if payload is not None:
data = json.dumps(payload).encode("utf-8")

req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
body = response.read().decode("utf-8")
return response.status, body
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
return exc.code, body

def wait_for_hasura(url, timeout=60):
deadline = time.time() + timeout
health_url = f"{url}/healthz"

while time.time() < deadline:
try:
status, body = request_json("GET", health_url, timeout=5)
if status == 200 and body.strip() == "OK":
return
except Exception:
pass
time.sleep(1)

raise RuntimeError(f"Hasura did not become healthy at {health_url}")

def metadata(url, payload):
return request_json("POST", f"{url}/v1/metadata", payload)

def query(url, payload):
return request_json("POST", f"{url}/v2/query", payload)

def is_success_or_known_duplicate(status, body):
if 200 <= status < 300:
return True

duplicate_markers = (
"already exists",
"already tracked",
"source with name",
"already present",
)
lower_body = body.lower()
return status == 400 and any(marker in lower_body for marker in duplicate_markers)

def ensure_source(url, source, database_url):
payload = {
"type": "pg_add_source",
"args": {
"name": source,
"configuration": {
"connection_info": {
"database_url": database_url,
"isolation_level": "read-committed",
"use_prepared_statements": False,
}
},
},
}
status, body = metadata(url, payload)
if not is_success_or_known_duplicate(status, body):
raise RuntimeError(f"pg_add_source failed: HTTP {status}: {body}")

print(f"[+] Source ready: {source}")

def quote_ident(value):
return '"' + value.replace('"', '""') + '"'

def ensure_bad_table(url, source, schema, table_name):
sql = (
f"CREATE SCHEMA IF NOT EXISTS {quote_ident(schema)};"
f"CREATE TABLE IF NOT EXISTS {quote_ident(schema)}.{quote_ident(table_name)} "
"(id serial primary key);"
)
status, body = query(
url,
{
"type": "run_sql",
"args": {
"source": source,
"sql": sql,
},
},
)
if not (200 <= status < 300):
raise RuntimeError(f"run_sql failed: HTTP {status}: {body}")

status, body = metadata(
url,
{
"type": "pg_track_table",
"args": {
"source": source,
"table": {
"schema": schema,
"name": table_name,
},
},
},
)
if not is_success_or_known_duplicate(status, body):
raise RuntimeError(f"pg_track_table failed: HTTP {status}: {body}")

print(f"[+] Problematic table ready: {schema}.{table_name}")

def crash_url(url, source, schema):
source_path = urllib.parse.quote(source, safe="")
schema_path = urllib.parse.quote(schema, safe="")
return f"{url}/console/data/{source_path}/schema/{schema_path}"

def open_browser(url, browser_app):
if sys.platform == "darwin":
command = ["open"]
if browser_app:
command.extend(["-a", browser_app])
command.append(url)
subprocess.run(command, check=True)
return

import webbrowser

if not webbrowser.open(url):
raise RuntimeError(f"Unable to open browser for {url}")

def main():
args = parse_args()
target = base_url(args.url)

print(f"[*] Target: {target}")
host, port = check_tcp(target)
print(f"[+] TCP reachable: {host}:{port}")

wait_for_hasura(target)
status, body = request_json("GET", f"{target}/v1/version")
print(f"[+] Hasura version: {body.strip() if status == 200 else 'unknown'}")

ensure_source(target, args.source, args.database_url)
ensure_bad_table(target, args.source, args.schema, args.table_name)

url_to_open = crash_url(target, args.source, args.schema)
print()
print("[*] Crash trigger URL:")
print(f" {url_to_open}")
print()
print("[*] Expected result:")
print(" The Hasura Console opens the Data tree and renders the global Error page.")
print(" The visible error should mention an invalid regular expression and")
print(" 'Unterminated group'.")

if args.no_open:
print("[*] --no-open set; browser was not opened.")
return

open_browser(url_to_open, args.browser_app)
time.sleep(max(args.wait_seconds, 0))
print("[+] Browser opened. Verify the visible Console tab shows the Error page.")

if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"[!] {exc}", file=sys.stderr)
sys.exit(1)

```

### Screenshots or Screencast

Image

### Please provide any traces or logs that could help here.
Relevant stack trace from the browser Console:

SyntaxError: Invalid regular expression: /\/data\/default\/schema\/public\/(tables|functions|views)\/.../: Unterminated group
at new RegExp ()
...
Relevant code locations:

// TreeView.tsx
new RegExp(
`\\/data\\/${currentSource}\\/schema\\/${currentSchema}\\/(tables|functions|views)\\/${item.name}\\/`
)
// HighlightText.tsx
text.split(new RegExp(`(${highlightedText})`, 'gi'))

### Any possible solutions/workarounds you're aware of?
Escape user/database-controlled strings before interpolating them into a regex, or avoid regex construction entirely for route/string matching.

Example:

const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
Then apply it to currentSource, currentSchema, item.name, and highlightedText before constructing RegExp.

For TreeView.tsx, another option is to compare normalized path segments without RegExp.

### Keywords
console, data sidebar, TreeView, HighlightText, RegExp, regex escaping, SyntaxError, database object names, client-side crash

Contributor guide

Open the contributing guide

Research direction

Start with frontend/libs/console/legacy-ce/src/lib/components/Services/Data/TreeView.tsx and the RegExp construction described there, then inspect HighlightText.tsx for the search-term case. Run poc/hasura_console_regex_crash_poc.py against a local Hasura instance to reproduce the failure. Done means database object names and search terms containing regex metacharacters render without a Console crash or altered literal matching.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, typescript
Domain
databases, frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.