d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { getGitLabApiBase, normalizeGitLabHost, UnsafeGitLabHostError } from '@/tools/gitlab/utils'
|
|
|
|
describe('normalizeGitLabHost', () => {
|
|
it('defaults to gitlab.com when the host is empty, blank, or not a string', () => {
|
|
expect(normalizeGitLabHost(undefined)).toBe('gitlab.com')
|
|
expect(normalizeGitLabHost(null)).toBe('gitlab.com')
|
|
expect(normalizeGitLabHost('')).toBe('gitlab.com')
|
|
expect(normalizeGitLabHost(' ')).toBe('gitlab.com')
|
|
expect(normalizeGitLabHost(42)).toBe('gitlab.com')
|
|
})
|
|
|
|
it('strips protocol and trailing slashes from a self-managed host', () => {
|
|
expect(normalizeGitLabHost('gitlab.example.com')).toBe('gitlab.example.com')
|
|
expect(normalizeGitLabHost('https://gitlab.example.com')).toBe('gitlab.example.com')
|
|
expect(normalizeGitLabHost('http://gitlab.example.com/')).toBe('gitlab.example.com')
|
|
expect(normalizeGitLabHost(' https://gitlab.example.com// ')).toBe('gitlab.example.com')
|
|
})
|
|
|
|
it('preserves an explicit port and IDN punycode labels', () => {
|
|
expect(normalizeGitLabHost('gitlab.example.com:8443')).toBe('gitlab.example.com:8443')
|
|
expect(normalizeGitLabHost('xn--80ak6aa92e.com')).toBe('xn--80ak6aa92e.com')
|
|
})
|
|
|
|
it('rejects hosts that could redirect the request authority (SSRF / token exfiltration)', () => {
|
|
const unsafe = [
|
|
'legit.com@evil.com',
|
|
'user:pass@evil.com',
|
|
'gitlab.com#@evil.com',
|
|
'gitlab.com /api',
|
|
'line\nbreak.com',
|
|
'evil.com/path',
|
|
'evil.com?x=1',
|
|
'[::1]',
|
|
'a..b.com',
|
|
'.gitlab.com',
|
|
'gitlab.com.',
|
|
]
|
|
for (const host of unsafe) {
|
|
expect(() => normalizeGitLabHost(host), host).toThrow(UnsafeGitLabHostError)
|
|
}
|
|
})
|
|
|
|
it('accepts bare IP literals at the STRUCTURAL layer by design (private/metadata IPs are rejected later by the fetch-layer DNS guard)', () => {
|
|
// This guard is structural only — it prevents authority confusion (userinfo,
|
|
// path, whitespace). SSRF to private/loopback/metadata addresses is the
|
|
// responsibility of validateUrlWithDNS / secureFetchWithValidation at fetch
|
|
// time, the single SSRF chokepoint shared by tools, webhooks, and connectors.
|
|
// These hosts are therefore structurally valid here, then blocked at fetch.
|
|
expect(normalizeGitLabHost('127.0.0.1')).toBe('127.0.0.1')
|
|
expect(normalizeGitLabHost('169.254.169.254')).toBe('169.254.169.254')
|
|
expect(normalizeGitLabHost('localhost')).toBe('localhost')
|
|
})
|
|
})
|
|
|
|
describe('getGitLabApiBase', () => {
|
|
it('builds the v4 REST base for the default and self-managed hosts', () => {
|
|
expect(getGitLabApiBase(undefined)).toBe('https://gitlab.com/api/v4')
|
|
expect(getGitLabApiBase('gitlab.example.com')).toBe('https://gitlab.example.com/api/v4')
|
|
expect(getGitLabApiBase('https://gitlab.example.com:8443/')).toBe(
|
|
'https://gitlab.example.com:8443/api/v4'
|
|
)
|
|
})
|
|
|
|
it('propagates rejection of unsafe hosts', () => {
|
|
expect(() => getGitLabApiBase('legit.com@evil.com')).toThrow(UnsafeGitLabHostError)
|
|
})
|
|
})
|