Files
Project-SDE-WP-Suite/KNOWN-ISSUES.md
n.siegfried c99ef08cf1 Record the smoketest auth gap and the browser-verification gap
Two things surfaced while building the User Directory that are worth a decision
rather than a mention in a handover.

3. server/smoketest.py has no login step, so auth_gate 401s every check after
   /api/health. DEPLOYMENT.md presents it as the way to prove the stack works
   end-to-end, including a docker compose exec invocation, so the documented
   verification path reports failure on a healthy system -- the failure mode most
   likely to be believed. Rated Medium for that reason. Predates the login
   portal; confirmed unrelated to this branch by stashing it and re-running. The
   Admin Console's in-browser smoke test is the working equivalent today.

4. users.js and wp-sidenav.js have never been executed -- no JS engine on the
   machine they were written on. Logged as a verification gap, not a defect, with
   what WAS checked (server tests, delimiter balance, handler resolution, id
   targets) and what only a browser can settle (layout, transitions, focus trap).
   Includes the five-step manual pass that closes it, and the hard-reload note,
   since sw.js bumped to wp-suite-shell-v6 and a soft reload serves the old
   shell.

Both entries follow the file's existing shape: what is wrong, what it costs, why
it is still open, what closing it takes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:06:47 -07:00

14 KiB

Known issues — Work Package Suite

Defects and limitations we know about and have decided not to fix yet. An entry here is a commitment to a decision, not a bug tracker: it says what is wrong, what it costs, why it is still open, and what closing it takes.

Anything genuinely urgent does not belong here — it belongs in the next deploy.

Close an entry by deleting it in the same commit that fixes it.

# Issue Severity Raised Status
1 XSS via SOP discipline names in the WP creator Medium (internal), High if externally reachable 2026-08-05 Open
2 Archived projects: the two big apps don't grey out their own controls Low 2026-08-05 Open
3 server/smoketest.py cannot authenticate — every run fails with 401 Medium 2026-08-05 Open
4 User Directory and nav drawer have not been run in a browser Low (verification gap, not a known defect) 2026-08-05 Open

1. XSS via SOP discipline names in the WP creator

Files: html/wp-creation-app.js lines 684, 723, 727, 729 · escaping helper at line 63 Predates: the 2026-08-05 archive/admin-console work. Not introduced by it.

What is wrong

Discipline names are rendered into inline event handlers escaped with esc(), which maps ' to &#39;. That is correct for text and wrong here. The browser decodes entities in an attribute value before the JavaScript parser sees it, so &#39; becomes a bare ' inside the handler's string literal and closes it early.

// html/wp-creation-app.js:684 — esc() is not sufficient for a handler argument
onchange="toggleDiscipline('${esc(d)}',this.checked)"

Escaping for an inline handler has to happen in this order: backslash, then quote (for the JS string literal), then HTML (for the attribute carrying it). esc() only does the last part.

How it is reached

  1. gov_disciplines (html/work-package-suite.html:219) is a free-text field. Its value is comma-split with no validation at work-package-suite-app.js:1224.
  2. It is saved into sops.data and syncs to the server via ProjectData.pushSOP.
  3. Every other member of that project pulls it with pullProject() and renders it in the WP creator — so this is stored and cross-user, and it fires on page load rather than needing the victim to click anything.

Any project_user on the job can set it while the SOP is a draft (after the SOP is marked complete it takes project_admin). The victim is anyone who opens the WP creator for that project, which includes administrators.

What it costs

The likely cost is a broken screen, not an attack. A discipline named Owner's Equipment — an ordinary thing to type — produces a syntax error in the handler, so the discipline pill and its scope-step buttons silently stop responding. No error message, nothing a field user can diagnose.

The security ceiling is project_user → admin. The session cookie is HttpOnly so the token cannot be read, but the injected code does not need it: it runs in the victim's page and can call any API the victim can, including POST /api/auth/users/{id}/role.

The project_super_user role (added 2026-08-05) widens the set of victims whose session is worth stealing, without raising the ceiling. Previously only an app admin's session could create accounts or change permissions; now a super user's can too, within the projects they administer. The ceiling is unchanged — it was already admin — but the odds of landing on a session that can mint an account go up, and a super user is likelier than an admin to be reading a WP creator on a live job. It is one more reason the accidental-breakage case is not the only one that matters.

Two controls that look like they would contain this do not:

  • CSP does not mitigate it. nginx-wp-suite.conf:58 serves script-src 'self' 'unsafe-inline', and 'unsafe-inline' is what permits inline event handlers in the first place.
  • The CSRF gate does not mitigate it. _csrf_ok (server/app.py:67) only requires a same-origin Origin, and code running inside our own page is same-origin.

Why it is still open

The suite is internal, behind a login, on the corporate network, with a small set of named employee accounts and no anonymous input path. Exploiting it means an employee deliberately attacking colleagues, and the audit log carries their name on the SOP edit. The accidental-breakage case is far more likely to be met than the malicious one.

Re-rate this as High and fix it immediately if any of these become true: the suite is exposed outside the corporate network, accounts are issued to subcontractors or clients, or self-registration is added.

Note that the second of those got easier to reach without anyone deciding to: a Project Super User can now issue accounts on their own job without an app admin involved, so "accounts are issued to subcontractors" can become true by ordinary delegated use rather than by a policy change. Worth checking the directory occasionally against who is actually on staff.

What closing it takes

Small — roughly half an hour. The helper already exists; it was added to the SOP builder on 2026-08-05 for the same bug in custom constraint names:

// html/work-package-suite-app.js:1120
function escHandlerArg(v){ return escAttr(String(v==null?'':v).replace(/\\/g,'\\\\').replace(/'/g,"\\'")); }
  1. Add the same helper to html/wp-creation-app.js alongside esc().
  2. Use it at lines 684, 723, 727 and 729 in place of esc(d).
  3. Sweep the other inline handlers in that file for the same pattern. The remaining ones interpolate server-generated ids that check_id() already constrains to a safe charset, or hardcoded enum values, so they are not currently reachable — converting them anyway keeps the pattern from coming back.
  4. Confirm with a discipline named Owner's Equipment: the pill must respond to clicks and the name must display intact.

The equivalent fix on the admin side is jsq() in html/console-util.js (it moved out of html/admin.js on 2026-08-05 when the User Directory started needing it) — same ordering, same reasoning, worth reading before starting.


2. Archived projects: the two big apps don't grey out their own controls

Files: html/wp-creation-app.js, html/work-package-suite-app.js Raised: 2026-08-05, with the project-archiving work.

What is wrong

Archiving a project freezes it server-side — every write returns 409 (see require_project_writable in server/app.py, and the Archiving a project section of DEPLOYMENT.md). The front end tells the user, but does not stop them: wp-chrome.js shows a read-only banner and sets data-wp-archived="1" on the document element, and nothing reads that attribute yet. So on an archived project the WP creator and the SOP builder still present working Save and Issue buttons.

What it costs

Low, and it fails safe — the server refuses the write, so nothing is corrupted and no data is lost. The cost is wasted effort and a confusing moment: someone deep- linked to an archived job can fill in a form and only learn it was refused when the sync indicator reports the change did not save.

Reaching an archived project at all takes a deep link or a stale tab, since it is gone from every picker, switcher and search — which is why this is a rough edge rather than a defect.

Why it is still open

Gating every control in two large single-page apps is materially bigger than the archive feature itself, and the server is the real enforcement boundary either way. The banner plus the sync indicator were judged enough for a first release.

What closing it takes

data-wp-archived is already on the document element for exactly this purpose. Either add [data-wp-archived] rules in wp-chrome.css that disable and dim the save/issue controls, or add a boot check in each app that disables them and shows a read-only notice inline. Decide separately how the embedded creator (wp-creation-index.html) surfaces it, since it runs in an iframe where the shared app bar — and therefore the banner — is deliberately skipped.


3. server/smoketest.py cannot authenticate — every run fails with 401

Files: server/smoketest.py · documented in DEPLOYMENT.md §"Automated smoke test" (line ~147) Predates: the login portal. The script was written against an open API and was never updated when authentication landed.

What is wrong

The script has no login step — no call to /api/auth/login, no cookie jar, no credential arguments. auth_gate (server/app.py) refuses every /api/ route without a session cookie, so every check after the first fails:

- create project  (status=401)
- fetch project by id  (status=401)
...
Result: FAIL

Only /api/health passes, because it is on auth._EXEMPT_EXACT.

What it costs

The documented end-to-end verification path does not work, and hasn't for some time. DEPLOYMENT.md presents this as the way to prove "NGINX → FastAPI → PostgreSQL all work", including a docker compose exec invocation for use inside the api container. Anyone following it after a deploy gets a wall of 401s and has to work out for themselves whether the stack is broken or the script is.

The stack itself is fine, and there is a working equivalent: the Admin Console → End-to-end smoke test card runs the same sequence from the browser, where the session cookie already exists. html/admin.html describes it as mirroring smoketest.py, which is now the only place that sequence actually runs.

Rated Medium rather than Low because it is a verification tool that reports failure on a healthy system — the failure mode most likely to be believed and acted on.

Why it is still open

It was found while testing unrelated work (the User Directory) and is not a defect in the product. Fixing it means choosing how the script gets credentials, which is a small design decision about a deploy-time tool rather than a code fix, and it should not ride along inside a feature branch.

What closing it takes

Small — under an hour, stdlib only, matching the script's existing constraint.

  1. Add --user / --password arguments, defaulting to WP_SMOKE_USER / WP_SMOKE_PASSWORD from the environment so the runbook does not put a password on a command line.
  2. Install an http.cookiejar.CookieJar on the opener in call(), then POST /api/auth/login before the first check and assert it returned 200. There is a working reference for both steps in the throwaway harness written for this branch (Client in the scratch http_check.py), or in html/admin.js's in-browser version of the same flow.
  3. Note in DEPLOYMENT.md that the account needs access to the project the script creates — simplest is an admin account, since a project_user cannot create a project.
  4. Decide whether a missing credential is a hard failure or a skip with a clear message. A silent 401 wall is what caused this entry.

4. User Directory and nav drawer have not been run in a browser

Files: html/users.html, html/users.js, html/wp-sidenav.js, html/wp-sidenav.css, html/console.css Raised: 2026-08-05, with the Project Super User work.

What is wrong

This is a gap in what has been verified, not a known defect. The machine the code was written on has no JavaScript engine available (no node, deno, or npx), so these files have never been executed. What has been checked:

  • The server side they talk to: 93 scope/permission tests and 29 live HTTP tests through the real dependency stack, all passing.
  • Static analysis of the browser code: delimiter balance across all scripts (with a tokenizer that handles nested template interpolation), every inline handler resolving to a defined function, every getElementById target existing in its page, and no leftover references to the functions that moved out of admin.js.

What that cannot cover is anything only a browser decides: CSS layout, the drawer's open/close transitions and focus trap, event wiring, and how the three role renderings actually look with real data.

What it costs

Unknown by definition, bounded by blast radius. A syntax or boot error in users.js means a blank User Directory — obvious the moment anyone opens it, and it takes nothing else down: users.html is a new page, and the drawer is additive to pages that already worked without it. The server-side role and its scope rules are independently tested and unaffected either way.

The one thing to watch is console.css, which was extracted from admin.html's inline styles and is now shared. A rule lost in that move would show up as the Admin Console losing its dense-table styling — the same symptom the deploy runbook already tells you to look for ("one line per user", not three).

Why it is still open

It closes with one manual page load, which needs a browser signed in to a real deployment — not something to fake from a test harness.

What closing it takes

Minutes. Hard-reload first (Ctrl+Shift+R) — the service worker caches the app shell and sw.js bumped to wp-suite-shell-v6, so a normal reload can serve the old file list and make a good deploy look broken.

  1. Open users.html as an admin: the table renders, rows are one line tall, and the Permissions and Project role dropdowns are populated.
  2. Open it as a Project Super User: the blue scope banner names their project(s), the create form demands at least one project, and any account also on a job they don't administer shows as read-only with a reason on hover.
  3. Open it as a project user: six columns, no controls, no create form.
  4. Open field.html on a phone or a narrow window: the ☰ opens the drawer, Escape and the scrim close it, and Admin Console appears only for admins.
  5. Open admin.html and confirm the tables still look dense and correct — that is the console.css extraction check.

Delete this entry once those five are done.