Skip to content

Security hardening + OPAC fixes (v0.7.42)#294

Open
fabiodalez-dev wants to merge 4 commits into
mainfrom
security/scan-findings-20260725
Open

Security hardening + OPAC fixes (v0.7.42)#294
fabiodalez-dev wants to merge 4 commits into
mainfrom
security/scan-findings-20260725

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Release-prep branch for v0.7.42 — a security-hardening + bug-fix release. It closes a set of access-control, SSRF, open-redirect and data-exposure issues found in a full security audit of app/, public/ and installer/, and fixes three OPAC/CMS regressions reported by users.

Security (audit findings)

High

  • Access control on internal APIsGET /api/editori and GET /api/libri were reachable without a session and returned every column, exposing publisher PII (email, phone, tax code, referent contacts) and internal book fields (price, private notes, inventory numbers). Now behind the admin-session middleware, matching how the API docs already describe them.
  • Borrower privacy — a non-admin patron could open /admin/books/{id} and read every borrower's name, email and loan history. Borrower identity is now gated to admin/staff only.
  • Update privilege boundary — a staff user could upload+install an update package or trigger a full update over the app files (a route to code execution). Those actions (and saveToken) now require an explicit admin re-check.

Medium/Low — public search API no longer leaks borrower name/email; admin routes re-validate role+status against the DB each request (immediate demotion/suspension); settings no longer expose API keys / reCAPTCHA secret to staff; API keys accepted only via header, never ?api_key=; open redirect after login and on the language switch (/\ backslash bypass) fixed in all sanitizers; path-traversal on language-file upload; SSRF in the image proxy (redirect + DNS-rebinding) and the installer DB test; plaintext SMTP password in the installer; CSV/formula injection in two exports; reset-link Host-header poisoning; login timing side channel; rate-limit bypass via forged forwarding headers.

Two independent Opus reviewers audited the fixes; the one real regression they found (a staff save wiping the admin reCAPTCHA secret) and two minor residuals are fixed in the final commit.

Bug fixes

Database

  • migrate_0.7.42.sql — bumps the da_DK translation key count (6607 → 6611) for installs already on 0.7.41 (idempotent). New behavioural migration test.

Testing

  • PHPStan level 5 clean; full unit battery green (incl. new migration-0.7.42 and security-scan-findings tests); code-quality 15/15. The related-books and subtitle fixes were verified in a real browser at 5 viewport widths. Full E2E suite + reinstall regression to run before release.

Summary by CodeRabbit

  • Nuove funzionalità

    • Rafforzati i controlli di accesso per API, pagine amministrative e operazioni di aggiornamento.
    • Limitata la visualizzazione dei dati personali dei prestatori a amministratori e staff.
    • Le API accettano le chiavi solo tramite header.
    • Aggiunta protezione avanzata per password SMTP e link di reimpostazione.
  • Correzioni

    • Migliorata la sicurezza contro redirect indesiderati, SSRF, traversal e spoofing degli indirizzi IP.
    • Corretta la visualizzazione dei sottotitoli dei libri e della griglia dei libri correlati.
    • Migliorato l’upload dell’immagine Hero con policy CSP restrittive.
  • Documentazione

    • Aggiornate le note di rilascio e la documentazione API per la versione 0.7.42.

…orage

Six findings from a multi-agent security scan of app/, public/ and installer/,
each confirmed against the code and covered by a regression test
(tests/security-scan-findings.unit.php, 12/12):

- F1 (CWE-807, MEDIUM): RateLimitMiddleware keyed the login/forgot/reset
  throttle on client-supplied X-Forwarded-For without checking the peer, so a
  rotating header minted unlimited buckets and defeated the only brute-force
  control. It now honors forwarding headers only when REMOTE_ADDR is a
  configured trusted proxy (reuses HtmlHelper::isRemoteAddrTrustedProxy, made
  public); default Apache-direct keys on the real peer.
- F6 (CWE-807): same trusted-proxy gate applied to RememberMeService::getClientIP
  so the security audit log / session UI cannot be fed a forged IP.
- F3 (CWE-862): private mode allow-listed /feed.xml, /sitemap and /llms.txt,
  leaking book titles/authors/descriptions and enumerating every catalog URL to
  logged-out visitors. Removed from ALLOWED_PREFIXES (still fully public when
  private mode is off).
- F5 (CWE-918): /api/plugins/proxy-image followed redirects and re-resolved DNS
  after its own public-IP check (TOCTOU rebinding). Now FOLLOWLOCATION=false
  (3xx refused), protocol pinned to HTTPS, and curl pinned via CURLOPT_RESOLVE
  to the exact IPs already validated as public.
- F2 (CWE-312): the installer wrote smtp_password to system_settings in
  cleartext, bypassing the app's AES-256-GCM at-rest encryption. It now encrypts
  with the same scheme inline (round-trips through SettingsEncryption::decrypt);
  aborts the step if no key is available rather than persisting plaintext.
- F4 (CWE-918): the installer DB connection test echoed the raw PDO error,
  an error-based oracle for internal port scanning on the unauthenticated
  test-connection path. Detail now goes to the server log; the client gets a
  generic, non-oracle message.

phpstan level 5 clean; full unit battery 67/67.
…trict CSP

Three OPAC/CMS regressions reported on 2026-07-25/26:

- Related books grid packed 6+ cards on high-resolution laptops. The
  auto-fill minmax(170px) floor let a wide grid fit too many columns.
  Each column is now at least a quarter of the row (or 200px), so the
  grid caps at 4 columns on wide screens and degrades to 3/2/1 as the
  viewport narrows; the <=480px swipe carousel is unchanged. Verified in
  a real browser at 1512/1100/834/640/390px -> 4/4/3/2/carousel.

- The book subtitle (sottotitolo) was never rendered on the public book
  page even when present (#293). Added it under the title, escaped the
  same way, shown only when non-empty. Verified it now displays.

- Hero background upload failed under a strict connect-src CSP (#292):
  the file-added handler did fetch(URL.createObjectURL(file.data)), and
  blob: is not always allowed by connect-src, so the image never reached
  the form. Uppy already provides a File in file.data — use it directly
  (mirrors settings/index.php's logo uploader); only fall back to
  fetching a preview URL when file.data is not a File. No blob: fetch on
  the normal path, so it works regardless of the site's CSP.
Findings from a multi-agent security audit of app/, public/ and installer/,
each confirmed against the code and fixed under the project's conventions
(soft-delete, escaping, \Throwable, admin re-checks). HIGH first:

- F1/F5 (CWE-862): GET /api/editori and /api/libri were unauthenticated and
  returned SELECT e.*/l.* — publisher PII (email, phone, tax code, referent
  contacts) and internal book fields (price, private notes, inventory numbers).
  Both routes now require AdminAuthMiddleware, matching the sibling admin API
  routes and the API docs.
- F2 (CWE-862): a standard/premium patron could read every borrower's name,
  email and loan history via /admin/books/{id}. Borrower PII is now not fetched
  for non-admin/staff and gated behind $isAdminOrStaff in the view (defaults to
  false so an unset flag can never leak PII).
- F3/F7 (CWE-269): a staff user could upload+install an update package or run a
  full update over the app files (path to RCE). installManualUpdate/uploadUpdate/
  performUpdate now require an inline admin re-check, like backup/restore.
- F8 (CWE-613): admin routes re-validate tipo_utente + stato against the DB each
  request (cached per request), so demotion/suspension takes effect immediately.
- F9 (CWE-863): the public search API no longer returns the borrower's name/
  email — availability and due date only.
- F11 (CWE-522): settings no longer expose API keys or the reCAPTCHA secret to
  staff; F12 (CWE-598): API keys accepted only via X-API-Key / Authorization
  header, never a ?api_key= query string.
- F4/F10 (CWE-601): open redirect after login and on the language switch — the
  `/\` backslash-authority bypass of the `//` guard is now rejected.
- F6 (CWE-22): path traversal in language-file upload — the locale code is
  validated and the resolved path confined before any file write.
- SSRF: image proxy no longer follows redirects and pins curl to the validated
  public IPs (DNS-rebinding); installer DB test no longer echoes the raw PDO
  error (network oracle). F13: reset links fail closed on the Host header.
  F14: login dummy hash matches PASSWORD_DEFAULT cost (timing). F15/F16: CSV/TSV
  exports escape formula-leading cells. F1(prev)/F6(prev): rate-limit + audit IP
  only trust forwarding headers behind a configured proxy.

Release prep for v0.7.42:
- 4 new UI strings added to all 5 locales; da_DK key count 6607 -> 6611.
- migrate_0.7.42.sql bumps existing 0.7.41 installs to the new da_DK count
  (idempotent); migrate_0.7.41.sql updated in lockstep with the locale file.
- New behavioural migration test (tests/migration-0.7.42.unit.php, 5/5).
- version.json -> 0.7.42; README "What's New" section; docs/api.MD updated.

phpstan level 5 clean; full unit battery 68/68; code-quality 15/15.
…d, redirect

Two Opus reviewers (correctness + regression) audited the audit fixes and
agreed on one real regression the F11 fix introduced, plus two minor residuals:

- F11 regression (was HIGH): the reCAPTCHA secret input is rendered disabled
  for staff, and disabled inputs are not submitted — so updateContactSettings
  read the absent field as '' and persisted it, silently WIPING the
  admin-configured secret and fail-opening reCAPTCHA on the public contact
  form. Now the secret key is admin-only in the save handler: for non-admins
  it is dropped from the settings map, so neither a normal staff save nor a
  forged POST can overwrite the stored value.
- F3/F7 residual (LOW): saveToken() (persists the GitHub update token) lacked
  the inline admin re-check its sibling update actions have — added.
- PrestitiController::sanitizeRedirect (LOW): only blocked '//', not the '/\'
  backslash-authority bypass; aligned with the AuthController/LanguageController
  sanitizers (findings F4/F10). Admin+CSRF-gated, so not externally exploitable,
  fixed for consistency.

Everything else in the audit was verified complete by both reviewers.
phpstan level 5 clean.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bad50b98-6bc5-44de-bbf2-c12b78e6639d

📥 Commits

Reviewing files that changed from the base of the PR and between d22834e and 6c6743d.

📒 Files selected for processing (37)
  • README.md
  • app/Controllers/Admin/LanguagesController.php
  • app/Controllers/AuthController.php
  • app/Controllers/CollocazioneController.php
  • app/Controllers/LanguageController.php
  • app/Controllers/LibraryThingImportController.php
  • app/Controllers/LibriController.php
  • app/Controllers/PasswordController.php
  • app/Controllers/PrestitiController.php
  • app/Controllers/PublicApiController.php
  • app/Controllers/SettingsController.php
  • app/Controllers/UpdateController.php
  • app/Middleware/AdminAuthMiddleware.php
  • app/Middleware/ApiKeyMiddleware.php
  • app/Middleware/PrivateModeMiddleware.php
  • app/Middleware/RateLimitMiddleware.php
  • app/Routes/web.php
  • app/Support/HtmlHelper.php
  • app/Support/RememberMeService.php
  • app/Views/cms/edit-home.php
  • app/Views/frontend/book-detail.php
  • app/Views/libri/scheda_libro.php
  • app/Views/settings/advanced-tab.php
  • app/Views/settings/contacts-tab.php
  • docs/api.MD
  • installer/classes/Validator.php
  • installer/database/migrations/migrate_0.7.41.sql
  • installer/database/migrations/migrate_0.7.42.sql
  • installer/steps/step6.php
  • locale/da_DK.json
  • locale/de_DE.json
  • locale/en_US.json
  • locale/fr_FR.json
  • locale/it_IT.json
  • tests/migration-0.7.42.unit.php
  • tests/security-scan-findings.unit.php
  • version.json
 ___________________________________________________
< Vibe code cleanup in progress... Expect swearing. >
 ---------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/scan-findings-20260725

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Action performed

Full review triggered.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 56 minutes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant