Preventive, not incident-driven. No LUPMIS host is known to have been
compromised; these rules exist so the most common automated attack against a
PHP deployment cannot succeed.
Scanners probe every public host for /.env (database passwords, API keys) and
/.git/. The second is the more damaging: given /.git/index together with
/.git/objects an attacker can reconstruct the whole repository, including its
history — so any credential ever committed is exposed even after rotation.
That is directly relevant here, because the minio-uploads integration has had
access keys written into its PHP files and those files are tracked in Gitea.
- public/.htaccess: deny hidden paths and files that should never be served.
- docs/apache-hardening.conf: the same rules for vhost/server config, which is
where they belong — .htaccess depends on AllowOverride. Also covers logging
the real client address through the openresty proxy (mod_remoteip), and
blocking at the proxy so requests never reach the application.
Two details that are easy to get wrong, and are handled:
- <FilesMatch "^\."> does NOT stop /.git/config. FilesMatch tests the basename
only, and there the basename is "config". Blocking a hidden directory needs a
rule that sees the whole path: mod_rewrite in .htaccess, <DirectoryMatch> in
server config.
- The SPA fallback only rewrites paths that do not exist (!-f), so a real .env
on disk skips it and is served as a plain file. The deny rules therefore run
before the fallback, not after.
.well-known is exempt, or ACME certificate renewal would silently break.
Verified against a live Apache instance with a planted .env and .git tree:
/.env and /./.env return 403; /.git/config, /.git/HEAD, /.git/objects/... and
/backup.sql return 404; /.well-known/acme-challenge/..., /assets/*.js and
/manifest.json still return 200. Nothing in the document root is caught — the
only dotfile there is .htaccess, which Apache never serves.
These rules are damage limitation. The fix is to keep .git and .env out of a
document root: deploy build output rather than a working copy, and hold secrets
in environment variables outside the served tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session / district correctness
- public/index.php: add a /?logout=1 endpoint that destroys the PWA's own PHP
session (session_destroy + expire PHPSESSID + clear sso_auth_token, then
redirect to the SSO portal). Logout previously cleared only the SSO cookie,
leaving the PHPSESSID session — and its frozen district_id — intact, which is
why a reassigned user kept loading the old district across logout/login.
- SSO token is validated once per session, at login (unchanged first-login
logic). A district transfer is now picked up on the next logout→login, which
is correct precisely because logout finally tears the session down. No
periodic SSO polling.
- main.js: the menu Logout button routes through /?logout=1 and wipes
district-scoped local caches first. Logout is blocked while offline — a
session can only be created online, so an offline logout would strand the
user with no way back in (and would not actually reach the server).
- main.js: enforceDistrictConsistency() clears district-scoped caches when the
session district changes between loads; the district boundary is cached under
a per-district key (district_boundary_<id>) so one district's geometry can
never be served for another.
GPS coordinate format
- New "GPS Coordinate Format" setting (Lat/Lon · UTM · Both) in the Settings
panel; the navbar read-out renders the chosen format and repaints the current
fix immediately on change. Self-contained WGS84→UTM converter in
geo-utils.js, verified against an independent Redfearn-series implementation.
ol-ext touch cursor
- MapView gates the TouchCursor to genuine touch-only devices via matchMedia
(any-pointer: fine / any-hover: hover); hybrid touchscreen laptops keep the
normal cursor. Reactive to pointer-capability changes.
- Service worker v11 → v12 (new shell). docs/SSO_Session_Refresh_Proposal.md
documents the implemented approach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>