Files
twotalesanimation 81efab9cb0 Security hardening, DB connection fix, and handover documentation
Prepares the site for handover to the incoming committee. Three groups of
change: security fixes, a production stability fix, and documentation.

SECURITY

- upload_blog_image.php accepted unauthenticated uploads with no extension
  or MIME validation, into a web-served directory. Anyone could upload and
  execute a .php file. Now requires a session, checks blog ownership, and
  runs through validateFileUpload() with a new blog_image type.
- Added uploads/.htaccess and assets/uploads/.htaccess disabling the PHP
  engine, so an upload bypass still cannot execute. Force-added: .gitignore
  contains a bare .htaccess rule.
- blog/autosave.php updated posts by blog_id alone, letting any logged-in
  member overwrite anyone's post. Now scoped to the author, admin override
  retained. It also silently reassigned authorship to the editing admin;
  the existing author is now preserved.
- add_to_cart.php took item_price and user_id from POST, so drinks could be
  charged at any price to any member's tab, unauthenticated. Price and name
  are now looked up server-side from bar_items. Its require paths were also
  relative and broken.
- Session cookies now httponly/secure/samesite with strict mode, set in both
  .htaccess and .user.ini since php_flag and .user.ini apply under different
  SAPIs. The 30 minute idle timeout that login set but never checked is now
  enforced. Google login now regenerates the session id.
- Forced HTTPS. Required, because Secure cookies are never sent over plain
  HTTP and login would fail silently. LetsEncrypt acme-challenge is exempted
  above the redirect so certificate renewal keeps working.
- display_errors off; .htaccess now blocks *.log, error_log, .git and
  dotfiles. Debug logs containing member IDs, raw webhook bodies and payment
  signatures were publicly fetchable.
- Removed the IKHOKHA_BYPASS_SIGNATURE switch and the logging of raw webhook
  bodies, app IDs and signatures. Replaced a hardcoded personal Gmail
  recipient with the optional DEVELOPER_EMAIL variable.
- Added auth to fetch_users, fetch_bar_tabs, fetch_drinks, create_bar_tab and
  submit_order. Converted get_tab_total to a prepared statement.
- Tightened 11 mkdir(0777) calls to 0755.
- Fixed the validate_login rewrite, which pointed at a nonexistent path and
  only worked by falling through to the generic .php rule.

DATABASE STABILITY

openDatabaseConnection() opened a new connection on every call across 73 call
sites, and the server was hitting "Too many connections" in production. It now
returns one shared handle per request, reusing the connection connection.php
already opened. The 40 conn->close() calls in functions.php are removed, since
closing the shared handle breaks every later caller; PHP releases it at end of
request. A liveness check reconnects transparently if anything closes it.

Verified no function was lost (99 before and after), every removed close was
inside a braced block, and no transaction spans a helper that would open a
nested one.

DOCUMENTATION AND TOOLING

- docs/HANDOVER.md: operator guide covering architecture, routing, roles, the
  membership lifecycle, payments, backups, POPIA obligations, environment
  variables, known issues and a handover checklist.
- docs/FUNCTIONS.md: reference for the helpers in functions.php.
- scripts/backup.sh: nightly database and uploads backup, written outside the
  web root, with rotation and verification. Dumps are written to a temp file
  and only promoted after passing gzip, CREATE TABLE and completion-marker
  checks, so a failed run can never overwrite a good backup.
- .deployignore and .gitattributes added; .gitattributes pins LF on shell
  scripts and server config because core.autocrlf is true on Windows.
- Removed leftover debris: src/api/test_log.php and .htaccess1.

All 22 modified PHP files pass php -l. The backup script was tested against a
stub mysqldump for healthy, truncated and failing dumps, and for rotation.
Nothing was run against a live database or web server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:56:32 +02:00

277 lines
12 KiB
ApacheConf

# URL Rewrite Rules - Maps old URLs to new directory structure during migration
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Allow LetsEncrypt validation (must stay above the HTTPS redirect)
RewriteRule ^\.well-known/acme-challenge/ - [L]
# === FORCE HTTPS ===
# Required: the session cookie is set with the Secure flag, so a plain-HTTP
# request would never receive it and login would silently fail.
# HTTP_X_FORWARDED_PROTO covers the case where a proxy terminates TLS.
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP:X-Forwarded-Proto} !=https
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
# Don't rewrite existing files or directories
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# === STRIP .PHP EXTENSION ===
# Redirect /page.php to /page (301 permanent redirect)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
# Internally rewrite /page to /page.php if page.php exists
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ $1.php [L]
# === AUTH PAGES ===
RewriteRule ^login$ src/pages/auth/login.php [L]
RewriteRule ^register$ src/pages/auth/register.php [L]
RewriteRule ^forgot_password$ src/pages/auth/forgot_password.php [L]
RewriteRule ^reset_password$ src/pages/auth/reset_password.php [L]
RewriteRule ^verify$ src/pages/auth/verify.php [L]
RewriteRule ^resend_verification$ src/pages/auth/resend_verification.php [L]
RewriteRule ^change_password$ src/pages/auth/change_password.php [L]
RewriteRule ^update_password$ src/pages/auth/update_password.php [L]
# === MEMBERSHIP PAGES ===
RewriteRule ^membership$ src/pages/memberships/membership.php [L]
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
RewriteRule ^renewal_payment$ src/pages/memberships/renewal_payment.php [L]
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
# === BOOKING PAGES ===
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
RewriteRule ^trips$ src/pages/bookings/trips.php [L]
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
RewriteRule ^driver_training$ src/pages/bookings/driver_training.php [L]
# === SHOP PAGES ===
RewriteRule ^view_cart$ src/pages/shop/view_cart.php [L]
RewriteRule ^add_to_cart$ src/pages/shop/add_to_cart.php [L]
RewriteRule ^bar_tabs$ src/pages/shop/bar_tabs.php [L]
RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
# === GALLERY PAGES ===
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
# === EVENTS & BLOG PAGES ===
RewriteRule ^events$ src/pages/events/events.php [L]
RewriteRule ^blog$ src/pages/blog/blog.php [L]
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
RewriteRule ^instapage$ src/pages/events/instapage.php [L]
# === OTHER PAGES ===
RewriteRule ^about$ src/pages/other/about.php [L]
RewriteRule ^base4$ src/pages/other/base4.php [L]
RewriteRule ^contact$ src/pages/other/contact.php [L]
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
RewriteRule ^track-map$ src/pages/track-map.php [L]
RewriteRule ^404$ src/pages/other/404.php [L]
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
RewriteRule ^bush_mechanics$ src/pages/other/bush_mechanics.php [L]
RewriteRule ^indemnity$ src/pages/other/indemnity.php [L]
RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
# === PAYMENT RETURN PAGES ===
RewriteRule ^success$ src/pages/payment/success.php [L]
RewriteRule ^failure$ src/pages/payment/failure.php [L]
RewriteRule ^cancel$ src/pages/payment/cancel.php [L]
# === ADMIN PAGES ===
RewriteRule ^admin_trips_events_courses$ src/admin/admin_trips_events_courses.php [L]
RewriteRule ^admin_bookings$ src/admin/admin_bookings.php [L]
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
RewriteRule ^admin_transactions$ src/admin/admin_transactions.php [L]
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
RewriteRule ^admin_prices$ src/admin/admin_prices.php [L]
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
RewriteRule ^admin_courses$ /src/admin/admin_courses.php [L,QSA]
RewriteRule ^manage_courses$ /src/admin/manage_courses.php [L,QSA]
# === API/AJAX ENDPOINTS ===
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
RewriteRule ^fetch_drinks$ src/api/fetch_drinks.php [L]
RewriteRule ^fetch_bar_tabs$ src/api/fetch_bar_tabs.php [L]
RewriteRule ^get_campsites$ src/api/get_campsites.php [L]
RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
# === PROCESSORS ===
RewriteRule ^process_course$ /src/processors/process_course.php [L,QSA]
RewriteRule ^delete_course$ /src/processors/delete_course.php [L,QSA]
# validate_login.php lives at the repo root, not in src/processors/ - the
# restructure never moved it. This previously pointed at a nonexistent path
# and only worked by falling through to the generic .php rule above.
RewriteRule ^validate_login$ validate_login.php [L]
RewriteRule ^register_user$ src/processors/register_user.php [L]
RewriteRule ^process_application$ src/processors/process_application.php [L]
RewriteRule ^process_booking$ src/processors/process_booking.php [L]
RewriteRule ^process_camp_booking$ src/processors/process_camp_booking.php [L]
RewriteRule ^process_course_booking$ src/processors/process_course_booking.php [L]
RewriteRule ^process_trip_booking$ src/processors/process_trip_booking.php [L]
RewriteRule ^process_membership_payment$ src/processors/process_membership_payment.php [L]
RewriteRule ^process_payments$ src/processors/process_payments.php [L]
RewriteRule ^process_eft$ src/processors/process_eft.php [L]
RewriteRule ^submit_order$ src/processors/submit_order.php [L]
RewriteRule ^submit_pop$ src/processors/submit_pop.php [L]
RewriteRule ^process_signature$ src/processors/process_signature.php [L]
RewriteRule ^create_bar_tab$ src/processors/create_bar_tab.php [L]
RewriteRule ^update_application$ src/processors/update_application.php [L]
RewriteRule ^update_user$ src/processors/update_user.php [L]
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [L]
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
RewriteRule ^logout$ src/processors/logout.php [L]
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
RewriteRule ^process_event$ src/processors/process_event.php [L]
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
RewriteRule ^delete_event$ src/processors/delete_event.php [L]
RewriteRule ^save_album$ src/processors/save_album.php [L]
RewriteRule ^update_album$ src/processors/update_album.php [L]
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
# Blog routes
RewriteRule ^admin_blogs$ src/admin/admin_blogs.php [L]
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
</IfModule>
# Never render PHP errors to visitors - they leak paths, SQL and credentials.
# Errors still go to the server error log.
php_flag display_errors Off
php_flag log_errors On
# php_value error_reporting -1
# RedirectMatch 403 ^/\.well-known
# Session hardening. Duplicated in .user.ini on purpose: php_flag/php_value
# only apply under mod_php, .user.ini only under CGI/FastCGI. Whichever SAPI
# this host uses, one of the two takes effect. Needed because ~25 files call
# session_start() directly instead of including src/config/session.php.
php_flag session.cookie_httponly On
php_flag session.use_strict_mode On
php_flag session.use_only_cookies On
php_flag session.cookie_secure On
php_value session.cookie_samesite Lax
Options -Indexes
# Block source, dumps, archives and log files from being fetched directly.
# NOTE: "*.log" and the extensionless "error_log" matter most - PHP writes
# member IDs, raw webhook bodies and payment signatures into them.
# .txt is deliberately NOT blocked wholesale so robots.txt and domain
# verification files keep working; the debug .txt files are named below.
<FilesMatch "\.(env|sql|bak|zip|tar|gz|ini|log|lock|dist|sh)$">
Require all denied
</FilesMatch>
<FilesMatch "^(error_log|_webhook_hit\.txt|_admin_tx_debug\.log|composer\.(json|lock))$">
Require all denied
</FilesMatch>
# Block dotfiles and VCS metadata (.git, .env, .gitignore, ...)
RedirectMatch 404 /\.(git|svn|hg|env|idea|vscode)(/|$)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
ErrorDocument 404 /404.php
<RequireAll>
Require all granted
Require not ip 4.222.252.98
Require not ip 4.222.252.97
</RequireAll>
<Files .env>
Order allow,deny
Deny from all
</Files>
# ALL CUSTOM ENTRIES SHOULD GO ABOVE THIS LINE
# BEGIN IWORX header
# This file was created by InterWorx-CP
# You may modify this file, but any changes made between
# BEGIN IWORX and END IWORX tags may be lost on future
# updates. Additionally, changes NOT made between these
# tags will not be recognized in the SiteWorx interface.
# END IWORX header
# BEGIN IWORX accesscontrol
# END IWORX accesscontrol
# BEGIN IWORX errordocs
# END IWORX errordocs
# BEGIN IWORX mimetypes
# END IWORX mimetypes
# BEGIN IWORX handlers
# END IWORX handlers
# BEGIN IWORX charset
# END IWORX charset
# BEGIN IWORX redirects
# END IWORX redirects
# BEGIN IWORX phpvars
# END IWORX phpvars
# BEGIN IWORX dirindex
# END IWORX dirindex
# BEGIN IWORX hotlink
# END IWORX hotlink
# BEGIN IWORX passwordprotection
# END IWORX passwordprotection