Skip to content

Commit 8487ac5

Browse files
committed
fix(elasticsearch): reject a surviving colon in a decoded Cloud ID component
1 parent 264fcca commit 8487ac5

2 files changed

Lines changed: 41 additions & 15 deletions

File tree

‎apps/sim/tools/elasticsearch/utils.test.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,33 @@ describe('parseCloudId', () => {
6969

7070
/**
7171
* `extractPortFromName` splits at the last colon, so a second colon survives
72-
* in the *name* half, where the `#@?/\` reject set does not catch it. Left
73-
* unchecked this builds `https://<uuid>.<host>:<port>:<port>`, which throws a
74-
* bare `TypeError: Invalid URL` inside the transport instead of naming the
75-
* real problem.
72+
* in the *name* half. Both halves are numeric here on purpose: the all-digits
73+
* port check passes, so only the reject set can catch it. Left unchecked this
74+
* assembles `https://<uuid>.<host>:9243:5`, which surfaces as a bare
75+
* `TypeError: Invalid URL` inside the transport instead of naming the real
76+
* problem.
7677
*/
77-
it('rejects a second colon in the parent domain rather than building an invalid authority', () => {
78+
it('rejects a second colon in the parent domain when both halves are numeric', () => {
79+
const hostile = cloudId(`${PARENT_DOMAIN}:9243:5$${ES_UUID}$${KIBANA_UUID}`)
80+
expect(() => parseCloudId(hostile)).toThrow(/Invalid Cloud ID/)
81+
})
82+
83+
it('rejects a second colon in the parent domain when the service port is explicit', () => {
84+
const hostile = cloudId(`${PARENT_DOMAIN}:9243:5$${ES_UUID}:9200$${KIBANA_UUID}`)
85+
expect(() => parseCloudId(hostile)).toThrow(/Invalid Cloud ID/)
86+
})
87+
88+
it('rejects a second colon in the Elasticsearch component', () => {
89+
const hostile = cloudId(`${PARENT_DOMAIN}$${ES_UUID}:9243:5$${KIBANA_UUID}`)
90+
expect(() => parseCloudId(hostile)).toThrow(/Invalid Cloud ID/)
91+
})
92+
93+
/**
94+
* Distinct from the cases above: here the trailing half is non-numeric, so
95+
* the all-digits port check rejects it before the reject set is reached.
96+
* Kept separate so neither check can silently stop carrying its own case.
97+
*/
98+
it('rejects a second colon whose trailing half is not a port', () => {
7899
const hostile = cloudId(`${PARENT_DOMAIN}:9243:evil@x$${ES_UUID}:9243$${KIBANA_UUID}`)
79100
expect(() => parseCloudId(hostile)).toThrow(/Invalid Cloud ID/)
80101
})

‎apps/sim/tools/elasticsearch/utils.ts‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@ import type { ElasticsearchBaseParams } from '@/tools/elasticsearch/types'
77
const DEFAULT_CLOUD_PORT = '443'
88

99
/**
10-
* Characters that must not appear in a decoded Cloud ID component. An `@` would
11-
* turn the rest of the authority into a host and send the credential headers to
12-
* an attacker-controlled origin; `#`, `?` and `/` truncate the authority.
13-
* Mirrors the `strings.IndexAny(component, "#@?/")` reject set in Beats, plus
14-
* `\`, which the WHATWG URL parser treats as a path separator for special
15-
* schemes and which therefore truncates the authority exactly as `/` does.
10+
* Characters that must not appear in a decoded Cloud ID component name. An `@`
11+
* would turn the rest of the authority into a host and send the credential
12+
* headers to an attacker-controlled origin; `#`, `?` and `/` truncate the
13+
* authority. Mirrors the `strings.IndexAny(component, "#@?/")` reject set in
14+
* Beats, plus two additions:
1615
*
17-
* A `:` is deliberately absent — `extractPortFromName` has already split each
18-
* component at its last colon, so a surviving colon means a second one, which
19-
* the all-digits port check below rejects.
16+
* - `\`, which the WHATWG URL parser treats as a path separator for special
17+
* schemes and which therefore truncates the authority exactly as `/` does.
18+
* - `:`, because `extractPortFromName` has already split the component at its
19+
* last colon, so a colon surviving in the *name* half means the component
20+
* carried two. The all-digits port check below does not catch that when the
21+
* trailing half is numeric: `found.io:9243:5` yields name `found.io:9243`
22+
* and port `5`, which assembles `https://<uuid>.found.io:9243:5` and fails
23+
* as a bare `TypeError: Invalid URL` inside the transport. A hostname cannot
24+
* contain a colon, so rejecting it here cannot refuse a legitimate ID.
2025
*/
21-
const CLOUD_ID_REJECTED_CHARACTERS = /[#@?/\\]/
26+
const CLOUD_ID_REJECTED_CHARACTERS = /[#@?/\\:]/
2227

2328
/**
2429
* Splits a Cloud ID component of the form `name:port` at its last colon.

0 commit comments

Comments
 (0)