mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +03:00
fix(live): switch live-poll to execFileSync, validate ids strictly (#124)
* fix(live): switch live-poll to execFileSync, validate ids strictly
live-poll.mjs built the live-accept invocation with execSync and string
interpolation of event.id and event.variantId. Both fields originate in
the browser; validateEvent only checked truthiness, so shell metacharacters
in either field would land in the shell-parsed command.
Real exploitability is gated by the per-session token (loopback only,
unguessable UUID), so risk is low. The construction itself is structurally
unsafe though, and the fix is small.
- live-poll.mjs: execSync(string) → execFileSync('node', argv). Drops the
hand-rolled single-quote wrap for --param-values; execFileSync passes
each arg as a discrete argv slot, no shell parsing.
- live-server.mjs validateEvent: tighten id and variantId to match the
actual generator shapes (8 hex chars and 1-3 digit numeric strings).
Defense in depth so any value reaching downstream code is inert by
construction.
- live-server.test.mjs: add three regression tests covering accept/discard
rejection of shell-metachar ids and non-numeric variantIds. Update the
three existing fixture ids to match the new pattern.
Reported in #122.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: refresh pnpm-lock.yaml to match package.json
Cloudflare Pages runs pnpm install --frozen-lockfile and was failing on
ERR_PNPM_OUTDATED_LOCKFILE: the lockfile was missing entries for
@ai-sdk/anthropic, @ai-sdk/openai, @anthropic-ai/claude-agent-sdk,
@anthropic-ai/sdk, @google/genai, ai, modern-screenshot, zod, and had
stale specifiers for jsdom, marked, playwright, wrangler, puppeteer.
Drift was introduced when package.json was last edited without a lockfile
regen. Running pnpm install --lockfile-only resolves it; verified with
pnpm install --frozen-lockfile (clean install succeeds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Paul Bakaus
Claude Opus 4.7
parent
7e0ce5e6b1
commit
9a5d0e71a9
@@ -8,7 +8,7 @@
|
||||
* npx impeccable poll --reply <id> error "msg" # Reply with error
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
@@ -146,13 +146,12 @@ Options:
|
||||
? ['--id', event.id, '--discard']
|
||||
: ['--id', event.id, '--variant', event.variantId];
|
||||
if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) {
|
||||
// Pass through a JSON blob; the shell-safe wrap uses single quotes because
|
||||
// values are finite {id, number|string|boolean} pairs from a validated payload.
|
||||
scriptArgs.push('--param-values', `'${JSON.stringify(event.paramValues).replace(/'/g, "'\\''")}'`);
|
||||
scriptArgs.push('--param-values', JSON.stringify(event.paramValues));
|
||||
}
|
||||
try {
|
||||
const out = execSync(
|
||||
`node "${acceptScript}" ${scriptArgs.join(' ')}`,
|
||||
const out = execFileSync(
|
||||
'node',
|
||||
[acceptScript, ...scriptArgs],
|
||||
{ encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }
|
||||
);
|
||||
event._acceptResult = JSON.parse(out.trim());
|
||||
|
||||
@@ -131,11 +131,21 @@ const VISUAL_ACTIONS = [
|
||||
'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive',
|
||||
];
|
||||
|
||||
// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars)
|
||||
// and variantIds via String(small integer). Restrict to those shapes so
|
||||
// any value that reaches a downstream child_process or DOM selector is
|
||||
// inert by construction.
|
||||
const ID_PATTERN = /^[0-9a-f]{8}$/;
|
||||
const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); }
|
||||
function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); }
|
||||
|
||||
function validateEvent(msg) {
|
||||
if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message';
|
||||
switch (msg.type) {
|
||||
case 'generate':
|
||||
if (!msg.id || typeof msg.id !== 'string') return 'generate: missing id';
|
||||
if (!isValidId(msg.id)) return 'generate: missing or malformed id';
|
||||
if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action';
|
||||
if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8';
|
||||
if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context';
|
||||
@@ -145,8 +155,8 @@ function validateEvent(msg) {
|
||||
if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array';
|
||||
return null;
|
||||
case 'accept':
|
||||
if (!msg.id) return 'accept: missing id';
|
||||
if (!msg.variantId) return 'accept: missing variantId';
|
||||
if (!isValidId(msg.id)) return 'accept: missing or malformed id';
|
||||
if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId';
|
||||
if (msg.paramValues !== undefined) {
|
||||
if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) {
|
||||
return 'accept: paramValues must be an object';
|
||||
@@ -154,7 +164,7 @@ function validateEvent(msg) {
|
||||
}
|
||||
return null;
|
||||
case 'discard':
|
||||
return msg.id ? null : 'discard: missing id';
|
||||
return isValidId(msg.id) ? null : 'discard: missing or malformed id';
|
||||
case 'exit':
|
||||
return null;
|
||||
case 'prefetch':
|
||||
|
||||
Reference in New Issue
Block a user