|
|
@@ -562,24 +562,45 @@ note. Do not delete entries — they're history.
|
|
|
|
|
|
### R01-N16 — PhpSpreadsheet 3.4 — pin and watch CVE feed
|
|
|
- **Severity**: MEDIUM (advisory; depends on actual installed version).
|
|
|
-- **Status**: open.
|
|
|
-- **Where**: `composer.json` line 12 (`^3.4`), `composer.lock` (not read).
|
|
|
-- **What**: PhpSpreadsheet has a long history of XML-related vulns
|
|
|
- (XXE etc.) when reading XLSX. Version 3.4 disables XXE by default but the
|
|
|
- caret range allows minor upgrades, which is good — but operators may not
|
|
|
- rebuild promptly.
|
|
|
-- **Why it matters**: Importer is admin-only, but admins can still feed a
|
|
|
- hostile workbook (insider risk, compromised email, etc.).
|
|
|
-- **Suggested fix**:
|
|
|
- - Document upgrade cadence (Dockerfile rebuild) in admin manual.
|
|
|
- - Keep `setReadDataOnly(false)` (already correct — needed for fill colour
|
|
|
- parsing) but explicitly call `LIBXML_NOENT | LIBXML_DTDLOAD` to off if
|
|
|
- we ever reach to lower-level XML APIs.
|
|
|
- - Run `composer audit` in CI.
|
|
|
+- **Status**: fixed-in-`ef9b9b8` — went with the doc + tooling path, no
|
|
|
+ CI hook (this repo has no CI today; adding GitHub Actions would
|
|
|
+ introduce a new external dependency the operator hasn't asked for).
|
|
|
+ New `bin/audit.sh` wraps `composer audit --locked --no-interaction`
|
|
|
+ inside `sprint_planer_web-app:latest`, so the audit reflects the
|
|
|
+ exact dependency tree the live container ships and not whatever the
|
|
|
+ host PHP happens to have. Honours `SPRINT_PLANER_IMAGE` for
|
|
|
+ non-default tags. Refuses cleanly when docker is missing or the
|
|
|
+ image hasn't been built. Today's baseline: "No security
|
|
|
+ vulnerability advisories found." `doc/admin-manual.md` §5.5 grew a
|
|
|
+ "Composer dependency cadence" block — recommended rebuild rhythm
|
|
|
+ (after every git pull, monthly minimum), the auditor invocation,
|
|
|
+ and a copy-pasteable weekly-cron snippet that pipes a non-zero exit
|
|
|
+ to `mail`. PhpSpreadsheet itself stays at the `^3.4` caret range —
|
|
|
+ minor upgrades will land on each `docker compose build --no-cache`
|
|
|
+ with no manifest change required. No new tests (script is operator
|
|
|
+ tooling, not application code).
|
|
|
|
|
|
### R01-N17 — Login `state`/PKCE storage in session — verify lifetime
|
|
|
-- **Severity**: MEDIUM (compliance; needs runtime check).
|
|
|
-- **Status**: open.
|
|
|
+- **Severity**: MEDIUM (usability hazard; not a vulnerability).
|
|
|
+- **Status**: accepted-by-design — confirmed by reading
|
|
|
+ `vendor/jumbojett/openid-connect-php/src/OpenIDConnectClient.php`. The
|
|
|
+ library uses fixed session keys `openid_connect_state`,
|
|
|
+ `openid_connect_nonce`, `openid_connect_code_verifier` (lines
|
|
|
+ 1789-1866); the methods `setSessionKey()` / `getSessionKey()` /
|
|
|
+ `unsetSessionKey()` are `protected` so a subclass *could* namespace
|
|
|
+ them, but the namespace would then have to round-trip via a separate
|
|
|
+ short-lived cookie (the OIDC `state` parameter is generated and read
|
|
|
+ by the library itself, not by us). The realistic failure mode is
|
|
|
+ small: the second tab clobbers the first tab's `state`, the first
|
|
|
+ tab's callback hits the `if ($_REQUEST['state'] !== $this->getState())
|
|
|
+ { throw 'Unable to determine state' }` guard at line 322, the user
|
|
|
+ is bounced to `/?auth_error=1`, and they retry. That guard is the
|
|
|
+ *correct* OIDC state-mismatch behaviour — it's what stops a CSRF on
|
|
|
+ the OIDC callback. The "fix" (subclass + transient flow-id cookie,
|
|
|
+ ~80 LoC + tests) trades real complexity for a one-line UX nit. Not
|
|
|
+ worth it for a single-admin tool. `doc/admin-manual.md` §5.6
|
|
|
+ "Tabbed sign-in note" documents the rule for operators: complete
|
|
|
+ one sign-in at a time. No code change.
|
|
|
- **Where**: `src/Auth/OidcClient.php` + `src/Controllers/AuthController.php`
|
|
|
callback.
|
|
|
- **What**: `jumbojett/openid-connect-php` stores its `state` and PKCE
|
|
|
@@ -589,13 +610,32 @@ note. Do not delete entries — they're history.
|
|
|
bounce back. If two browser tabs initiate auth flows simultaneously, the
|
|
|
second overwrites the first's state. Not a direct vuln but a usability
|
|
|
hazard.
|
|
|
-- **Suggested fix**: Confirm the library's per-flow state isolation; if it
|
|
|
- stores under fixed keys, file an upstream issue or wrap with a per-flow
|
|
|
- random suffix.
|
|
|
+- **Suggested fix**: NONE. Decision recorded — see Status.
|
|
|
|
|
|
### R01-N18 — Email-claim trust path during OIDC upsert
|
|
|
- **Severity**: MEDIUM.
|
|
|
-- **Status**: open.
|
|
|
+- **Status**: fixed-in-`a0b717e` — new `App\Auth\OidcClaims::resolveEmail
|
|
|
+ ($claims, $oid)` is the single decision point for the audit-trail
|
|
|
+ email. The `preferred_username` fallback is removed entirely (the
|
|
|
+ actual hazard the finding flagged: user-controlled in some Entra
|
|
|
+ tenants). `claims.email` is honoured when present, EXCEPT when the
|
|
|
+ issuer explicitly marks it `email_verified === false` — that case
|
|
|
+ falls back to the documented immutable label `entra:<oid>` so the
|
|
|
+ forensic trail still has a stable, non-spoofable actor. A *missing*
|
|
|
+ `email_verified` flag is treated as "no negative signal" because
|
|
|
+ Entra v2.0 work/school tokens do not emit it (the email there comes
|
|
|
+ from the directory and is server-controlled); requiring strict
|
|
|
+ `=== true` would flip every existing audit row to `entra:<oid>`
|
|
|
+ which is an unnecessary regression for the threat model. The
|
|
|
+ `oid`-or-`sub` check moved up so a missing identifier rejects the
|
|
|
+ callback BEFORE the email resolver runs (the resolver assumes a
|
|
|
+ non-empty oid for its fallback). Identity is still keyed by `oid`
|
|
|
+ / `sub` and is unaffected. New `tests/Auth/OidcClaimsTest.php` (10
|
|
|
+ cases) pins the matrix: verified-true / verified-absent /
|
|
|
+ verified-false / preferred-username-fallback-rejected / blank-email
|
|
|
+ / whitespace-trim / no-claims-at-all / non-bool-truthy
|
|
|
+ email_verified / non-scalar email defence. Suite: 252 / 683 (was
|
|
|
+ 242 / 673).
|
|
|
- **Where**: `src/Controllers/AuthController.php` lines 75-77.
|
|
|
- **What**: `email = $claims->email ?? $claims->preferred_username`. In Entra,
|
|
|
`preferred_username` is *not* guaranteed to be a verified email; in some
|
|
|
@@ -888,7 +928,14 @@ A reasonable cadence (do not treat as binding):
|
|
|
13. ~~**R01-N13** (fatal-error safety net)~~ — fixed in `d7dbfb5`.
|
|
|
14. ~~**R01-N14** (XLSX session cap + abandoned-token audit)~~ —
|
|
|
fixed in `d7dbfb5`.
|
|
|
-15. The rest as time permits.
|
|
|
+15. ~~**R01-N18** (drop preferred_username fallback, honour
|
|
|
+ email_verified)~~ — fixed in `a0b717e`.
|
|
|
+16. ~~**R01-N16** (composer-audit helper + admin-manual cadence)~~ —
|
|
|
+ fixed in `ef9b9b8`.
|
|
|
+17. ~~**R01-N17** (concurrent-tab OIDC state)~~ — accepted-by-design;
|
|
|
+ library uses fixed session keys, the realistic failure mode is a
|
|
|
+ one-line UX nit, see Status block.
|
|
|
+18. The rest as time permits.
|
|
|
|
|
|
Each fix should ship as its own commit per SPEC.md §14, with a follow-up
|
|
|
SPEC update if behaviour or config surface changes.
|