From 19db1a21f21746b5999fe299f47da56a52a30514 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Fri, 1 May 2026 14:23:18 -0400 Subject: [PATCH 1/3] feat: add isLoopbackUrl helper using URL parser --- src/lib/is-loopback-host.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/lib/is-loopback-host.ts diff --git a/src/lib/is-loopback-host.ts b/src/lib/is-loopback-host.ts new file mode 100644 index 00000000..f454095c --- /dev/null +++ b/src/lib/is-loopback-host.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']) + +/** + * Returns true when the given URL targets a loopback address. + * Parses the URL with the built-in URL constructor and compares + * the hostname exactly against known loopback values. + */ +export function isLoopbackUrl (url: string): boolean { + try { + const { hostname } = new URL(url) + return LOOPBACK_HOSTNAMES.has(hostname) + } catch { + return false + } +} From 233f4167d763105bdd05ca0064e367a69d024831 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Fri, 1 May 2026 14:23:23 -0400 Subject: [PATCH 2/3] fix: use URL parser for loopback check in HTTP clients --- src/lib/cloud-client.ts | 3 ++- src/lib/kibana-client.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/cloud-client.ts b/src/lib/cloud-client.ts index ddee9a87..6291db61 100644 --- a/src/lib/cloud-client.ts +++ b/src/lib/cloud-client.ts @@ -5,6 +5,7 @@ import type { HttpMethod } from '../cloud/types.ts' import { getResolvedConfig } from '../config/store.ts' +import { isLoopbackUrl } from './is-loopback-host.ts' /** * Parameters for a single Cloud API request. @@ -31,7 +32,7 @@ export class CloudClient { constructor(baseUrl: string, apiKey: string) { this.baseUrl = baseUrl.replace(/\/+$/, '') this.apiKey = apiKey - if (this.baseUrl.startsWith('http://') && !/localhost|127\.0\.0\.1/.test(this.baseUrl)) { + if (this.baseUrl.startsWith('http://') && !isLoopbackUrl(this.baseUrl)) { process.stderr.write('Warning: using plaintext HTTP. Credentials will be sent unencrypted.\n') } } diff --git a/src/lib/kibana-client.ts b/src/lib/kibana-client.ts index 07d334b6..015ad654 100644 --- a/src/lib/kibana-client.ts +++ b/src/lib/kibana-client.ts @@ -4,6 +4,7 @@ */ import { getResolvedConfig } from '../config/store.ts' +import { isLoopbackUrl } from './is-loopback-host.ts' export type KibanaHttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH' @@ -41,7 +42,7 @@ export class KibanaClient { const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64') this.authHeader = `Basic ${encoded}` } - if (this.baseUrl.startsWith('http://') && !/localhost|127\.0\.0\.1/.test(this.baseUrl)) { + if (this.baseUrl.startsWith('http://') && !isLoopbackUrl(this.baseUrl)) { process.stderr.write('Warning: using plaintext HTTP. Credentials will be sent unencrypted.\n') } } From be0d819cb281699a27622ad62bfdae6dd1c555f0 Mon Sep 17 00:00:00 2001 From: margaretjgu Date: Fri, 1 May 2026 14:23:27 -0400 Subject: [PATCH 3/3] test: add unit tests for isLoopbackUrl --- test/lib/is-loopback-host.test.ts | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/lib/is-loopback-host.test.ts diff --git a/test/lib/is-loopback-host.test.ts b/test/lib/is-loopback-host.test.ts new file mode 100644 index 00000000..0b914398 --- /dev/null +++ b/test/lib/is-loopback-host.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { isLoopbackUrl } from '../../src/lib/is-loopback-host.ts' + +describe('isLoopbackUrl', () => { + it('returns true for http://localhost', () => { + assert.equal(isLoopbackUrl('http://localhost'), true) + }) + + it('returns true for http://localhost:9200', () => { + assert.equal(isLoopbackUrl('http://localhost:9200'), true) + }) + + it('returns true for https://localhost:5601/path', () => { + assert.equal(isLoopbackUrl('https://localhost:5601/path'), true) + }) + + it('returns true for http://127.0.0.1', () => { + assert.equal(isLoopbackUrl('http://127.0.0.1'), true) + }) + + it('returns true for http://127.0.0.1:9200', () => { + assert.equal(isLoopbackUrl('http://127.0.0.1:9200'), true) + }) + + it('returns true for http://[::1]:9200', () => { + assert.equal(isLoopbackUrl('http://[::1]:9200'), true) + }) + + it('returns false for http://localhost.attacker.com', () => { + assert.equal(isLoopbackUrl('http://localhost.attacker.com'), false) + }) + + it('returns false for http://not-localhost:9200', () => { + assert.equal(isLoopbackUrl('http://not-localhost:9200'), false) + }) + + it('returns false for http://example.com/localhost', () => { + assert.equal(isLoopbackUrl('http://example.com/localhost'), false) + }) + + it('returns false for http://127.0.0.1.evil.com', () => { + assert.equal(isLoopbackUrl('http://127.0.0.1.evil.com'), false) + }) + + it('returns false for a remote HTTPS URL', () => { + assert.equal(isLoopbackUrl('https://my-cluster.es.cloud.elastic.co:9243'), false) + }) + + it('returns false for an invalid URL', () => { + assert.equal(isLoopbackUrl('not-a-url'), false) + }) +})