@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-
|
|
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
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
|
1517
|
+
let interfaces = {};
|
|
1505
1518
|
try {
|
|
1506
|
-
|
|
1519
|
+
interfaces = os.networkInterfaces();
|
|
1507
1520
|
} catch {
|
|
1508
|
-
|
|
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 (
|
|
1576
|
+
if (!allowAny && !allowedHosts.has(hostname)) {
|
|
1520
1577
|
return {
|
|
1521
1578
|
ok: false,
|
|
1522
1579
|
status: 403,
|
|
1523
|
-
message:
|
|
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-
|
|
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");
|
package/package.json
CHANGED