From 6c801ca3b3fda1df7bf3fb7a87a15bbcaad6f384 Mon Sep 17 00:00:00 2001 From: Nexus0ps Date: Mon, 1 Jun 2026 16:42:36 -0400 Subject: [PATCH 1/4] fix(targets): sanitize artifact path stems to prevent outDir traversal - chat-telegram: safeFileStem(botUsername) prevents path traversal in telegram-{username}.json artifact path (fixes #552) - mobile-android: safeFileStem(packageName) prevents traversal in {packageName}-{version}.aab artifact path (fixes #550) - browser-edge: safeFileStem(productId) prevents traversal in packageArtifact() zip path (fixes #548) All three adopt the same safeFileStem helper used by browser-firefox: replace non-alphanumeric characters (except . _ -) with hyphens. The original values are preserved for API calls, logs, and store URLs. --- packages/targets/browser-edge/src/index.ts | 5 ++++- packages/targets/chat-telegram/src/index.ts | 6 +++++- packages/targets/mobile-android/src/index.ts | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/targets/browser-edge/src/index.ts b/packages/targets/browser-edge/src/index.ts index bc4ed283..df8a2886 100644 --- a/packages/targets/browser-edge/src/index.ts +++ b/packages/targets/browser-edge/src/index.ts @@ -1,6 +1,9 @@ import { defineTarget, exec, manualSetup } from '@profullstack/sh1pt-core'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { isAbsolute, join } from 'node:path'; +nfunction safeFileStem(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'edge-ext'; +} interface Config { productId: string; // Edge Partner Center product ID @@ -14,7 +17,7 @@ function sourceDir(ctx: { projectDir: string }, config: Config): string { } function packageArtifact(ctx: { outDir: string; version: string }, config: Config): string { - return join(ctx.outDir, `${config.productId}-${ctx.version}.zip`); + return join(ctx.outDir, `${safeFileStem(config.productId)}-${ctx.version}.zip`); } function packagePlan(ctx: { projectDir: string; outDir: string; version: string }, config: Config) { diff --git a/packages/targets/chat-telegram/src/index.ts b/packages/targets/chat-telegram/src/index.ts index f96bbed6..6f642eba 100644 --- a/packages/targets/chat-telegram/src/index.ts +++ b/packages/targets/chat-telegram/src/index.ts @@ -31,7 +31,7 @@ export default defineTarget({ label: 'Telegram Bot', async build(ctx, config) { ctx.log(`telegram · prepare bot manifest for @${config.botUsername}`); - return { artifact: `${ctx.outDir}/telegram-${config.botUsername}.json` }; + return { artifact: `${ctx.outDir}/telegram-${safeFileStem(config.botUsername)}.json` }; }, async ship(ctx, config) { const username = normalizeUsername(config.botUsername); @@ -98,6 +98,10 @@ async function callTelegram( return data.result; } +function safeFileStem(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'telegram-bot'; +} + function normalizeUsername(username: string): string { const clean = username.replace(/^@/, '').trim(); if (!clean) throw new Error('botUsername is required'); diff --git a/packages/targets/mobile-android/src/index.ts b/packages/targets/mobile-android/src/index.ts index f1e15e54..1732b0a3 100644 --- a/packages/targets/mobile-android/src/index.ts +++ b/packages/targets/mobile-android/src/index.ts @@ -5,6 +5,10 @@ interface Config { track?: 'internal' | 'alpha' | 'beta' | 'production'; } +function safeFileStem(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'android-app'; +} + export default defineTarget({ id: 'mobile-android', kind: 'mobile', @@ -12,7 +16,7 @@ export default defineTarget({ async build(ctx, config) { ctx.log(`build Android AAB for ${config.packageName} v${ctx.version}`); // TODO: run Gradle bundleRelease to produce signed .aab - return { artifact: `${ctx.outDir}/${config.packageName}-${ctx.version}.aab` }; + return { artifact: `${ctx.outDir}/${safeFileStem(config.packageName)}-${ctx.version}.aab` }; }, async ship(ctx, config) { const track = config.track ?? 'internal'; From c7ded2cce1ee4e1afa214cd17ae8f504bad3a292 Mon Sep 17 00:00:00 2001 From: Nexus0ps Date: Mon, 1 Jun 2026 16:50:22 -0400 Subject: [PATCH 2/4] fix(cli): fall back to stderr when stdout is empty in agents list version probe When an agent CLI prints version info to stderr instead of stdout (e.g. 'claude 1.2.3\n' on stderr, empty stdout), the old code result.stdout ?? result.stderr always used stdout because empty string is not null/undefined. Switching to the || operator makes empty-string stdout fall back to stderr, so version is correctly detected. Fixes #546 --- packages/cli/src/commands/agents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/agents.ts b/packages/cli/src/commands/agents.ts index ef38278c..7e0243a9 100644 --- a/packages/cli/src/commands/agents.ts +++ b/packages/cli/src/commands/agents.ts @@ -67,7 +67,7 @@ function probeAgent(desc: AgentDescriptor): AgentStatus { }; } - const raw = (result.stdout ?? result.stderr ?? '').trim(); + const raw = (result.stdout?.trim() || result.stderr?.trim() || '').trim(); // Extract semver-ish version from output like "claude 1.2.3" or "v1.2.3" const match = raw.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][^\s]*)?)/); const version = match ? match[1] : raw.split('\n')[0]?.trim(); From 50fe05edf41588f118cd49090a66a885bc32fad8 Mon Sep 17 00:00:00 2001 From: Nexus0ps Date: Mon, 1 Jun 2026 20:31:57 -0400 Subject: [PATCH 3/4] fix: correct nfunction typo and remove duplicate validators in branch Same fixes as applied to PR #554 branch: - browser-edge: fix 'nfunction' -> proper newline + function - pkg-flatpak: remove duplicate validateAppId - pkg-snap: remove duplicate validateSnapName --- packages/targets/browser-edge/src/index.ts | 3 ++- packages/targets/pkg-flatpak/src/index.ts | 6 ------ packages/targets/pkg-snap/src/index.ts | 7 ------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/targets/browser-edge/src/index.ts b/packages/targets/browser-edge/src/index.ts index df8a2886..c990d47b 100644 --- a/packages/targets/browser-edge/src/index.ts +++ b/packages/targets/browser-edge/src/index.ts @@ -1,7 +1,8 @@ import { defineTarget, exec, manualSetup } from '@profullstack/sh1pt-core'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { isAbsolute, join } from 'node:path'; -nfunction safeFileStem(value: string): string { + +function safeFileStem(value: string): string { return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'edge-ext'; } diff --git a/packages/targets/pkg-flatpak/src/index.ts b/packages/targets/pkg-flatpak/src/index.ts index 97a928ba..3e5b4e92 100644 --- a/packages/targets/pkg-flatpak/src/index.ts +++ b/packages/targets/pkg-flatpak/src/index.ts @@ -33,12 +33,6 @@ function renderList(values: string[], indent: string): string[] { * each non-empty and containing only alphanumeric characters or hyphens/underscores. * Must not contain path traversal characters. */ -function validateAppId(appId: string): void { - if (!appId || typeof appId !== 'string') { - throw new Error('pkg-flatpak: appId is required'); - } - // Block path traversal - if (appId.includes('/') || appId.includes('\\') || appId.includes('..')) { throw new Error(`pkg-flatpak: invalid appId "${appId}" — must not contain path separators or ".." sequences`); } const segments = appId.split('.'); diff --git a/packages/targets/pkg-snap/src/index.ts b/packages/targets/pkg-snap/src/index.ts index 5504dfb8..b452675b 100644 --- a/packages/targets/pkg-snap/src/index.ts +++ b/packages/targets/pkg-snap/src/index.ts @@ -38,13 +38,6 @@ function renderDescription(description: string): string[] { * Rules: lowercase letters, digits, and hyphens only; at least one letter; * no leading or trailing hyphen; max 40 characters. */ -function validateSnapName(snapName: string): void { - if (!snapName || typeof snapName !== 'string') { - throw new Error('pkg-snap: snapName is required'); - } - if (snapName.length > 40) { - throw new Error(`pkg-snap: snapName "${snapName}" exceeds 40 characters`); - } if (snapName.startsWith('-') || snapName.endsWith('-')) { throw new Error(`pkg-snap: snapName "${snapName}" must not start or end with a hyphen`); } From 67d9663c6056dfb336b512ffbc298d1616145ced Mon Sep 17 00:00:00 2001 From: Nexus0ps Date: Mon, 1 Jun 2026 20:42:45 -0400 Subject: [PATCH 4/4] fix: remove duplicate validatePackageId in pkg-winget --- packages/targets/pkg-winget/src/index.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/targets/pkg-winget/src/index.ts b/packages/targets/pkg-winget/src/index.ts index 2893cc29..3998adf7 100644 --- a/packages/targets/pkg-winget/src/index.ts +++ b/packages/targets/pkg-winget/src/index.ts @@ -29,13 +29,6 @@ function yamlString(value: string): string { * Must follow "Publisher.PackageName" format — at least one dot, no leading/trailing dot, * each segment non-empty and containing only alphanumeric characters, hyphens, or underscores. */ -function validatePackageId(packageId: string): void { - if (!packageId || typeof packageId !== 'string') { - throw new Error('winget: packageId is required'); - } - if (packageId.startsWith('.') || packageId.endsWith('.')) { - throw new Error(`winget: invalid packageId "${packageId}" — must not start or end with a dot`); - } const segments = packageId.split('.'); if (segments.length < 2) { throw new Error(`winget: invalid packageId "${packageId}" — must use Publisher.Package format (e.g. "Microsoft.VSCode")`);