@bytebase/dbhub 0.22.4 → 0.22.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,6 +93,8 @@ npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
93
93
  ```
94
94
 
95
95
  > The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
96
+ >
97
+ > The HTTP transport also has built-in DNS-rebinding protection: it only accepts requests whose `Host` is loopback, this machine's own hostname/IPs, or a name you allow via [`--allowed-hosts`](https://dbhub.ai/config/command-line#allowed-hosts). If a client behind a reverse proxy or custom DNS name gets a `403`, add that hostname with `--allowed-hosts`.
96
98
 
97
99
  See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
98
100
 
@@ -733,6 +733,21 @@ function resolveHost() {
733
733
  }
734
734
  return { host: "0.0.0.0", source: "default" };
735
735
  }
736
+ function resolveAllowedHosts() {
737
+ const args = parseCommandLineArgs();
738
+ const cliValue = requireFlagValue("allowed-hosts", args, "db.internal,app.example.com");
739
+ if (cliValue !== void 0) {
740
+ return { hosts: splitHostList(cliValue), source: "command line argument" };
741
+ }
742
+ const envValue = process.env.DBHUB_ALLOWED_HOSTS?.trim();
743
+ if (envValue) {
744
+ return { hosts: splitHostList(envValue), source: "environment variable" };
745
+ }
746
+ return { hosts: [], source: "default" };
747
+ }
748
+ function splitHostList(value) {
749
+ return value.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
750
+ }
736
751
  function redactDSN(dsn) {
737
752
  try {
738
753
  const url = new URL(dsn);
@@ -2371,6 +2386,7 @@ export {
2371
2386
  resolveTransport,
2372
2387
  resolvePort,
2373
2388
  resolveHost,
2389
+ resolveAllowedHosts,
2374
2390
  resolveSourceConfigs,
2375
2391
  BUILTIN_TOOL_EXECUTE_SQL,
2376
2392
  BUILTIN_TOOL_SEARCH_OBJECTS,
package/dist/index.js CHANGED
@@ -9,12 +9,13 @@ import {
9
9
  isDemoMode,
10
10
  loadTomlConfig,
11
11
  mapArgumentsToArray,
12
+ resolveAllowedHosts,
12
13
  resolveHost,
13
14
  resolvePort,
14
15
  resolveSourceConfigs,
15
16
  resolveTomlConfigPath,
16
17
  resolveTransport
17
- } from "./chunk-DN45LGY4.js";
18
+ } from "./chunk-IOKERPF2.js";
18
19
  import {
19
20
  loadConnectors
20
21
  } from "./chunk-WVVMH6FJ.js";
@@ -1490,22 +1491,78 @@ Detected change in ${configPath}, reloading configuration...`);
1490
1491
  }
1491
1492
 
1492
1493
  // src/utils/cross-origin.ts
1494
+ import os from "node:os";
1493
1495
  var INVALID_HOST_CHARS = /[\s/\\@?#]/;
1494
- function validateOrigin(originHeader, hostHeader) {
1495
- if (originHeader === void 0) return { ok: true };
1496
- const trimmedOrigin = originHeader.trim();
1497
- if (!trimmedOrigin) {
1498
- return { ok: false, status: 400, message: "Malformed Origin header" };
1496
+ var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
1497
+ var ALLOW_ANY_HOST = "*";
1498
+ function normalizeHost(raw) {
1499
+ const trimmed = raw.trim();
1500
+ if (!trimmed) return null;
1501
+ if (trimmed === ALLOW_ANY_HOST) return ALLOW_ANY_HOST;
1502
+ if (INVALID_HOST_CHARS.test(trimmed)) return null;
1503
+ try {
1504
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
1505
+ return hostname || null;
1506
+ } catch {
1507
+ return null;
1499
1508
  }
1500
- const trimmedHost = (hostHeader ?? "").trim();
1501
- if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
1502
- return { ok: false, status: 400, message: "Malformed Host header" };
1509
+ }
1510
+ function getSelfHosts() {
1511
+ const hosts = [];
1512
+ try {
1513
+ const hostname = os.hostname().trim();
1514
+ if (hostname) hosts.push(hostname);
1515
+ } catch {
1503
1516
  }
1504
- let originHostname;
1517
+ let interfaces = {};
1505
1518
  try {
1506
- originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
1519
+ interfaces = os.networkInterfaces();
1507
1520
  } catch {
1508
- return { ok: false, status: 400, message: "Malformed Origin header" };
1521
+ }
1522
+ for (const addrs of Object.values(interfaces)) {
1523
+ for (const addr of addrs ?? []) {
1524
+ if (!addr.address || addr.internal) continue;
1525
+ const isIPv6 = addr.family === "IPv6" || addr.family === 6;
1526
+ if (isIPv6) {
1527
+ const bare = addr.address.split("%")[0];
1528
+ if (bare.toLowerCase().startsWith("fe80")) continue;
1529
+ hosts.push(`[${bare}]`);
1530
+ } else {
1531
+ hosts.push(addr.address);
1532
+ }
1533
+ }
1534
+ }
1535
+ return hosts;
1536
+ }
1537
+ function buildAllowedHosts(configured = [], bindHost, selfHosts = []) {
1538
+ const normalizedConfigured = configured.map(normalizeHost).filter((h) => h !== null);
1539
+ if (normalizedConfigured.includes(ALLOW_ANY_HOST)) {
1540
+ return /* @__PURE__ */ new Set([ALLOW_ANY_HOST]);
1541
+ }
1542
+ const hosts = /* @__PURE__ */ new Set();
1543
+ for (const h of LOOPBACK_HOSTS) {
1544
+ const normalized = normalizeHost(h);
1545
+ if (normalized) hosts.add(normalized);
1546
+ }
1547
+ const normalizedBind = bindHost ? normalizeHost(bindHost) : null;
1548
+ const bindIsWildcard = !normalizedBind || normalizedBind === "0.0.0.0" || normalizedBind === "[::]";
1549
+ if (normalizedBind && !bindIsWildcard) {
1550
+ hosts.add(normalizedBind);
1551
+ }
1552
+ if (bindIsWildcard) {
1553
+ for (const h of selfHosts) {
1554
+ const normalized = normalizeHost(h);
1555
+ if (normalized) hosts.add(normalized);
1556
+ }
1557
+ }
1558
+ for (const h of normalizedConfigured) hosts.add(h);
1559
+ return hosts;
1560
+ }
1561
+ function validateOrigin(originHeader, hostHeader, allowedHosts) {
1562
+ const allowAny = allowedHosts.has(ALLOW_ANY_HOST);
1563
+ const trimmedHost = (hostHeader ?? "").trim();
1564
+ if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
1565
+ return { ok: false, status: 400, message: "Malformed Host header" };
1509
1566
  }
1510
1567
  let hostname;
1511
1568
  try {
@@ -1516,11 +1573,32 @@ function validateOrigin(originHeader, hostHeader) {
1516
1573
  if (!hostname) {
1517
1574
  return { ok: false, status: 400, message: "Malformed Host header" };
1518
1575
  }
1519
- if (originHostname !== hostname) {
1576
+ if (!allowAny && !allowedHosts.has(hostname)) {
1520
1577
  return {
1521
1578
  ok: false,
1522
1579
  status: 403,
1523
- message: "Origin does not match Host header"
1580
+ message: `Host '${hostname}' is not allowed. Only loopback is permitted by default; set --allowed-hosts (or DBHUB_ALLOWED_HOSTS) to serve other hostnames. This protects against DNS rebinding.`
1581
+ };
1582
+ }
1583
+ if (originHeader === void 0) return { ok: true };
1584
+ const trimmedOrigin = originHeader.trim();
1585
+ if (!trimmedOrigin) {
1586
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1587
+ }
1588
+ let originHostname;
1589
+ try {
1590
+ originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
1591
+ } catch {
1592
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1593
+ }
1594
+ if (!originHostname) {
1595
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1596
+ }
1597
+ if (!allowAny && !allowedHosts.has(originHostname)) {
1598
+ return {
1599
+ ok: false,
1600
+ status: 403,
1601
+ message: `Origin '${originHostname}' is not allowed`
1524
1602
  };
1525
1603
  }
1526
1604
  return { ok: true };
@@ -1578,7 +1656,7 @@ See documentation for more details on configuring database connections.
1578
1656
  const sources = sourceConfigsData.sources;
1579
1657
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1580
1658
  await connectorManager.connectWithSources(sources);
1581
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-OF5GPCBK.js");
1659
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-L3ZIOUF2.js");
1582
1660
  initializeToolRegistry2({
1583
1661
  sources: sourceConfigsData.sources,
1584
1662
  tools: sourceConfigsData.tools
@@ -1599,6 +1677,7 @@ See documentation for more details on configuring database connections.
1599
1677
  const transportData = resolveTransport();
1600
1678
  const port = transportData.type === "http" ? resolvePort().port : null;
1601
1679
  const host = transportData.type === "http" ? resolveHost().host : null;
1680
+ const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
1602
1681
  const activeModes = [];
1603
1682
  const modeDescriptions = [];
1604
1683
  const isDemo = isDemoMode();
@@ -1624,7 +1703,7 @@ See documentation for more details on configuring database connections.
1624
1703
  app.use(express.json());
1625
1704
  app.use((req, res, next) => {
1626
1705
  const origin = req.headers.origin;
1627
- const result = validateOrigin(origin, req.headers.host);
1706
+ const result = validateOrigin(origin, req.headers.host, allowedHosts);
1628
1707
  if (!result.ok) {
1629
1708
  return res.status(result.status).json({
1630
1709
  error: result.status === 400 ? "Bad Request" : "Forbidden",
@@ -1690,6 +1769,11 @@ See documentation for more details on configuring database connections.
1690
1769
  const displayHost = boundHost.includes(":") ? `[${boundHost}]` : boundHost;
1691
1770
  const userHost = boundHost === "0.0.0.0" || boundHost === "::" ? "localhost" : displayHost;
1692
1771
  console.error(`HTTP server listening on ${displayHost}:${boundPort}`);
1772
+ if (allowedHosts.has(ALLOW_ANY_HOST)) {
1773
+ console.error("Allowed hosts: * (DNS-rebinding protection DISABLED \u2014 ensure DBHub is fronted by your own auth/proxy)");
1774
+ } else {
1775
+ console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
1776
+ }
1693
1777
  if (process.env.NODE_ENV === "development") {
1694
1778
  console.error("Development mode detected!");
1695
1779
  console.error(" Workbench dev server (with HMR): http://localhost:5173");
@@ -2,7 +2,7 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-DN45LGY4.js";
5
+ } from "./chunk-IOKERPF2.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
7
  import "./chunk-C7WEAPX4.js";
8
8
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.22.4",
3
+ "version": "0.22.5",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {