Add phased-execution planning structure
Build and Push Container / build-and-push (push) Successful in 11s
Build and Push Container / build-and-push (push) Successful in 11s
Introduce .agents/ (PLAN.md with locked architectural anchors, phase roadmap under phases/todo/) and AGENTS.md rules for agents working in the repo. Queues the pending phases: fix history XSS, simulate fetch commands, and nginx security headers.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# Phase 01 — Fix terminal history-recall XSS (audit M-1)
|
||||
|
||||
**Story:** n/a
|
||||
**Context:** `.agents/PLAN.md` §4 (terminal modules), `.agents/remediation_plan.md` finding M-1
|
||||
|
||||
## Objective
|
||||
Eliminate the DOM-XSS sink in the fake terminal's command-history recall:
|
||||
ArrowUp/ArrowDown currently rebuild the prompt line with
|
||||
`lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' '`
|
||||
where `cmdLine` is raw user input from `commandHistory`. Replace it with
|
||||
textContent-based construction so terminal input can never be injected as
|
||||
HTML, and lock the fix with a Playwright regression test.
|
||||
|
||||
## Dependencies
|
||||
— (none)
|
||||
|
||||
## Tasks
|
||||
1. `01_fix_history_recall.md` — Rewrite the ArrowUp/ArrowDown recall to build the prompt line with `createElement`/`textContent` (mirror the existing `updateDisplay()` pattern).
|
||||
2. `02_xss_regression_test.md` — Add a Playwright test proving a markup payload typed into the terminal is recalled as literal text, not DOM.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: the new regression test must cover (a) a payload that
|
||||
would create a DOM node if injected (`<b id="xss-marker">x</b>`), (b) a
|
||||
payload with an inline event handler (`<img src=x onerror=...`) asserting
|
||||
no script execution, and (c) a benign command (`whoami`) still recalled
|
||||
and executable.
|
||||
- Coverage: **>90%** on new/modified code (the two recall branches in
|
||||
`src/terminal.js`).
|
||||
- E2E: full Playwright suite (`npm test`) green — it already exercises
|
||||
history recall with benign commands and must not regress.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `grep -n "innerHTML" src/terminal.js` shows no occurrence combining
|
||||
`innerHTML` with `cmdLine`/history data (static prompt literals and the
|
||||
neofetch canned-output assignment are the only allowed `innerHTML` uses)
|
||||
- [ ] new XSS regression test present in `tests/e2e/02-terminal.spec.js` and passing
|
||||
- [ ] `./build.sh && npm test` fully green (44+ tests)
|
||||
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
|
||||
@@ -0,0 +1,49 @@
|
||||
# Task 01 — Rewrite history recall with the safe display path
|
||||
|
||||
**Phase:** `01_fix_history_xss` · **Story:** n/a
|
||||
|
||||
## Objective
|
||||
Remove the `innerHTML` sink from the terminal's ArrowUp/ArrowDown history
|
||||
recall by routing recall through the existing `updateDisplay(text)` function,
|
||||
which already builds the prompt line with `textContent`/`createElement`.
|
||||
|
||||
## Work
|
||||
1. `src/terminal.js` — in the terminal `keydown` handler, replace the three
|
||||
`lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' '`
|
||||
constructions with `updateDisplay(...)` calls:
|
||||
|
||||
- **ArrowUp branch** (`if (historyIndex < commandHistory.length - 1)`):
|
||||
delete the `innerHTML` line, the `newCursor` creation, and the
|
||||
`appendChild` — keep `historyIndex++` and `const cmdLine = ...`, then:
|
||||
```js
|
||||
updateDisplay(cmdLine);
|
||||
```
|
||||
- **ArrowDown branch, first case** (`if (historyIndex > 0)`): same
|
||||
replacement — `updateDisplay(cmdLine);`
|
||||
- **ArrowDown branch, else case** (`historyIndex = -1`): replace the
|
||||
`innerHTML`/cursor block with:
|
||||
```js
|
||||
updateDisplay('');
|
||||
```
|
||||
|
||||
`updateDisplay` already: removes any stale `.terminal-cursor`, clears the
|
||||
line via `textContent`, appends a `.terminal-prompt` span (respecting
|
||||
`isRoot` → `#` vs `$`), the text, and a fresh cursor (red when root).
|
||||
|
||||
Note the two intentional, behavior-preserving improvements this brings:
|
||||
(a) recalled prompts match the live typing state (cursor directly after
|
||||
the text, no trailing space); (b) after `sudo su -`, recall shows `#`
|
||||
instead of a stale `$`. Both are consistency fixes, not regressions.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: no new unit code — the existing E2E test
|
||||
`02-terminal.spec.js › history: ArrowUp/ArrowDown recall previous commands`
|
||||
exercises recall and execution of recalled commands and must stay green.
|
||||
- Coverage: **>90%** on the modified branches (covered by that E2E test plus
|
||||
the task-02 regression test).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `grep -n "innerHTML" src/terminal.js` — no remaining occurrence that
|
||||
involves `cmdLine` or history data
|
||||
- [ ] `./build.sh && npm test` fully green
|
||||
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
|
||||
@@ -0,0 +1,65 @@
|
||||
# Task 02 — Add the XSS regression test
|
||||
|
||||
**Phase:** `01_fix_history_xss` · **Story:** n/a
|
||||
|
||||
## Objective
|
||||
Add a Playwright E2E test that proves a markup payload typed into the
|
||||
terminal is recalled (ArrowUp) as **literal text**, never as DOM, and never
|
||||
executes. This test fails on the pre-fix (innerHTML) code and passes on the
|
||||
fixed code, so it guards against regressions/reverts.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/02-terminal.spec.js` — add the following test inside the
|
||||
`test.describe('Fake terminal — state and special commands', ...)` block
|
||||
(it reuses `openTerminal`, `typeCommand`, `currentPromptLine` from
|
||||
`./helpers`):
|
||||
|
||||
```js
|
||||
test('history recall renders markup payloads as literal text (no DOM injection)', async ({ page }) => {
|
||||
await openTerminal(page);
|
||||
|
||||
// 1) A payload that would create a DOM node if innerHTML-injected.
|
||||
await typeCommand(page, '<b id="xss-marker">pwned</b>');
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(page.locator('#xss-marker')).toHaveCount(0);
|
||||
await expect(currentPromptLine(page)).toContainText('<b id="xss-marker">pwned</b>');
|
||||
|
||||
// 2) An event-handler payload must not execute. `data:,` is an empty
|
||||
// data URI so the <img> fails to decode deterministically (no
|
||||
// network dependency) and onerror would fire if injected as HTML.
|
||||
await page.evaluate(() => { window.__xssFired = false; });
|
||||
await typeCommand(page, '<img src="data:," onerror="window.__xssFired=true">');
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await page.waitForTimeout(200); // allow onerror to fire if vulnerable
|
||||
expect(await page.evaluate(() => window.__xssFired)).toBe(false);
|
||||
await expect(page.locator('img[onerror]')).toHaveCount(0);
|
||||
await expect(currentPromptLine(page)).toContainText('<img src="data:," onerror="window.__xssFired=true">');
|
||||
|
||||
// 3) Benign recall still works and is executable.
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('.terminal-content > div').nth(-2)).toHaveText('optional');
|
||||
});
|
||||
```
|
||||
|
||||
Notes for the executor:
|
||||
- Step 3 walks back down through history (`<img…>`, then `<b…>`, then the
|
||||
prior real command) and executes it; adjust the final assertion to the
|
||||
benign command that is actually on top of the history at that point if
|
||||
the exact stack differs when you run it. The point is that recall +
|
||||
execute of a benign command still works.
|
||||
- Do **not** add an `onerror`-based assertion that depends on a real
|
||||
network fetch; keep it self-contained with `data:,`.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: this test **is** the regression coverage for the M-1 fix.
|
||||
It must: fail on innerHTML-based recall, pass on textContent-based recall.
|
||||
- Coverage: **>90%** on the modified recall branches (both payload classes +
|
||||
benign path covered).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] new test present in `tests/e2e/02-terminal.spec.js`
|
||||
- [ ] `./build.sh && npm test` fully green (45+ tests)
|
||||
- [ ] (sanity) temporarily reverting task 01's fix makes this test fail —
|
||||
confirms it is a genuine regression guard, then re-apply the fix
|
||||
Reference in New Issue
Block a user