Skip to content

Commit 0351567

Browse files
committed
fix(databricks): accept DoD, custom-URL, and trailing-dot workspace hosts
1 parent d447fc7 commit 0351567

2 files changed

Lines changed: 29 additions & 17 deletions

File tree

‎apps/sim/lib/core/security/input-validation.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,8 @@ function validateVendorHostedUrl(
11671167
if (!urlResult.isValid) return urlResult
11681168

11691169
const parsed = new URL(candidate)
1170-
const hostname = parsed.hostname.toLowerCase()
1170+
// A fully qualified name's trailing dot names the same host.
1171+
const hostname = parsed.hostname.toLowerCase().replace(/\.$/, '')
11711172
const allowed = suffixes.some(
11721173
(suffix) => (allowBareSuffix && hostname === suffix.slice(1)) || hostname.endsWith(suffix)
11731174
)
@@ -1267,15 +1268,20 @@ export function validateWorkdayTenantUrl(
12671268
}
12681269

12691270
/**
1270-
* Every production Databricks control-plane DNS zone, mirroring `ALL_ENVS` in the
1271-
* Databricks SDK (`databricks/sdk/environments.py`). The SDK's `.dev.*`/`.staging.*`
1272-
* zones are internal and deliberately omitted; the ones that are subdomains of a
1273-
* zone listed here (e.g. `.staging.cloud.databricks.com`) match by suffix anyway.
1271+
* Every production Databricks control-plane DNS zone. All but `.cloud.databricks.mil` mirror
1272+
* `ALL_ENVS` in the Databricks SDK (`databricks/sdk/environments.py`); the DoD zone comes from the
1273+
* Databricks AWS GovCloud docs. The SDK's `.dev.*`/`.staging.*` zones are internal and
1274+
* deliberately omitted; the ones that are subdomains of a zone listed here (e.g.
1275+
* `.staging.cloud.databricks.com`) match by suffix anyway. `.databricks.com` admits workspace
1276+
* custom URLs (`acme.databricks.com`) and subsumes the AWS and GCP zones, which stay listed so
1277+
* the rejection message names them.
12741278
*/
12751279
const DATABRICKS_ALLOWED_HOST_SUFFIXES = [
12761280
'.cloud.databricks.com',
12771281
'.cloud.databricks.us',
1282+
'.cloud.databricks.mil',
12781283
'.gcp.databricks.com',
1284+
'.databricks.com',
12791285
'.azuredatabricks.net',
12801286
'.databricks.azure.us',
12811287
'.databricks.azure.cn',
@@ -1288,6 +1294,8 @@ const DATABRICKS_ALLOWED_HOST_SUFFIXES = [
12881294
* every REST call is made against it. Example valid hosts:
12891295
* - dbc-1234abcd-5678.cloud.databricks.com (AWS)
12901296
* - dbc-1234abcd-5678.cloud.databricks.us (AWS GovCloud)
1297+
* - dbc-1234abcd-5678.cloud.databricks.mil (AWS GovCloud DoD)
1298+
* - acme.databricks.com (workspace custom URL)
12911299
* - adb-1234567890123456.7.azuredatabricks.net (Azure)
12921300
* - adb-1234567890123456.7.databricks.azure.us (Azure US Government)
12931301
* - adb-1234567890123456.7.databricks.azure.cn (Azure China)

‎apps/sim/tools/databricks/databricks.test.ts‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { inputValidationMock, inputValidationMockFns } from '@sim/testing'
1+
import { inputValidationMock } from '@sim/testing'
2+
import { partialToolRegistry } from '@sim/testing/mocks/tool-registry.mock'
23
import { describe, expect, it, vi } from 'vitest'
34

45
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
56

6-
/** Only this service's configs are needed; the full registry is ~6,000 modules. */
7-
vi.mock('@/tools/registry', async () => {
8-
const { partialToolRegistry } = await import('@sim/testing/mocks/tool-registry.mock')
9-
return { tools: partialToolRegistry(await import('@/tools/databricks')) }
10-
})
11-
7+
import * as databricksTools from '@/tools/databricks'
128
import { executeTool } from '@/tools/index'
139
import { tools } from '@/tools/registry'
1410

11+
/** Registers only this service's configs in the global registry mock; the full one is ~6,000 modules. */
12+
Object.assign(tools, partialToolRegistry(databricksTools))
13+
1514
type ToolParams = Record<string, unknown>
1615

1716
function urlBuilder(toolId: string): (params: ToolParams) => string {
@@ -40,15 +39,19 @@ const REQUEST_PARAMS = {
4039
const DATABRICKS_TOOL_IDS = Object.keys(tools).filter((id) => id.startsWith('databricks_'))
4140

4241
describe('databricks workspace host allowlist', () => {
42+
it('registers the Databricks tools', () => {
43+
expect(DATABRICKS_TOOL_IDS).not.toEqual([])
44+
})
45+
4346
it.each([
4447
'attacker.example.com',
4548
'https://attacker.example.com/',
4649
'dbc-1.cloud.databricks.com.attacker.example.com',
4750
'attacker.example.com/dbc-1.cloud.databricks.com',
4851
'dbc-1.cloud.databricks.com@attacker.example.com',
49-
'cloud.databricks.com',
52+
'databricks.com',
53+
'acme-databricks.com',
5054
])('refuses %s in every tool before a URL is built', (host) => {
51-
expect(DATABRICKS_TOOL_IDS.length).toBe(26)
5255
const accepted = DATABRICKS_TOOL_IDS.filter((id) => {
5356
try {
5457
urlBuilder(id)({ ...REQUEST_PARAMS, host })
@@ -60,16 +63,14 @@ describe('databricks workspace host allowlist', () => {
6063
expect(accepted).toEqual([])
6164
})
6265

63-
it('fails the tool call without sending the token to a foreign host', async () => {
66+
it('fails the tool call for a foreign host with the allowlist error', async () => {
6467
const result = await executeTool('databricks_list_clusters', {
6568
host: 'attacker.example.com',
6669
apiKey: 'dapi-test-token',
6770
})
6871

6972
expect(result.success).toBe(false)
7073
expect(result.error).toContain('host must be a Databricks-hosted domain')
71-
expect(inputValidationMockFns.mockValidateUrlWithDNS).not.toHaveBeenCalled()
72-
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
7374
})
7475

7576
it.each([
@@ -81,6 +82,9 @@ describe('databricks workspace host allowlist', () => {
8182
['dbc-a1b2.cloud.databricks.us', 'https://dbc-a1b2.cloud.databricks.us'],
8283
['adb-123.4.databricks.azure.us', 'https://adb-123.4.databricks.azure.us'],
8384
['adb-123.4.databricks.azure.cn', 'https://adb-123.4.databricks.azure.cn'],
85+
['dbc-a1b2.cloud.databricks.mil', 'https://dbc-a1b2.cloud.databricks.mil'],
86+
['https://acme.databricks.com/', 'https://acme.databricks.com'],
87+
['dbc-a1b2.cloud.databricks.com.', 'https://dbc-a1b2.cloud.databricks.com.'],
8488
])('builds the same request URLs for workspace host %s', (host, origin) => {
8589
const params = { ...REQUEST_PARAMS, host }
8690
expect(urlBuilder('databricks_list_clusters')(params)).toBe(`${origin}/api/2.0/clusters/list`)

0 commit comments

Comments
 (0)