IFS= read was blocked, and what changedPR #3454 · closes #3453 · compound-command-validator
The DENY branch matched any IFS= assignment, including the empty
one. So the idiomatic safe read loop was hard-blocked.
The command was not the operator's. OrchestKit's own ork:dream skill ships
that exact line at src/skills/dream/SKILL.md:437, so STEP 8 of the skill was
un-runnable on every invocation. It was found by running the skill, not by reading it.
// before if (/\$\{?IFS\}?/.test(unquoted) || /\bIFS=/.test(unquoted)) { findings.push('IFS manipulation detected'); }
The DENY exists for a real reason: redefining IFS re-tokenizes a command and
can hide it from the substring denylist. That reasoning holds for IFS=, and
IFS=$'\n'. It does not hold for IFS=:
read executes no command text, so nothing can hide behind itThe obvious patch is "allow any command that contains the benign form". That is a bypass: one harmless occurrence would launder a real manipulation elsewhere in the same command.
// naive, and exploitable: if (!/\bIFS=\s+read\b/.test(u) && /\bIFS=/.test(u)) deny(); // IFS=, cmd && IFS= read -> ALLOWED. wrong. // shipped: strip the benign form, judge the REMAINDER const ifsProbe = unquoted.replace(/\bIFS=(?:''|"")?(?=\s+read\b)/g, ''); if (/\$\{?IFS\}?/.test(ifsProbe) || /\bIFS=/.test(ifsProbe)) deny();
That mixed case is covered by a regression test, not just by argument.
| Command | Before | After |
|---|
This is not an isolated bug. Same shape, three times:
Every one matched a syntactic shape instead of the evasive capability that shape sometimes carries. Worth a sweep of the remaining DENY branches rather than a fourth one-off.