Problem: <video> elements on tfe.php had no <track kind="captions"> element,
violating WCAG 4.1.2 (name, role, value) for video content.
Changes:
- public/tfe.php: collect all text/vtt files from the thesis file list before
rendering; skip standalone rendering of .vtt entries; for each MP4 emit a
<track kind="captions" srclang="fr" label="Sous-titres" default> pointing
to the N-th VTT file (N-th video paired with N-th caption in document order)
- public/media.php: add text/vtt to allowed MIME list; normalise finfo
text/plain -> text/vtt for .vtt files; add vtt branch to cache/header
block (Content-Type: text/vtt; charset=utf-8, 1-day cache)
- public/admin/actions/formulaire.php: allow .vtt uploads (text/vtt MIME,
vtt extension); normalise text/plain finfo result; set file_type='caption'
for VTT files so they are distinguishable from other thesis files
- public/admin/add.php: extend files field accept attr to include .vtt;
update hint text to document the VTT sidecar convention
VTT files uploaded under theses/ inherit the same access_type visibility
gate in media.php as all other thesis content (403 for access_type_id=3).
- Add getThesisAccessTypeId(int $id): ?int — replaces raw SELECT in tfe.php
- Add getCoverPathsForTheses(array $ids): array — replaces raw SELECT/IN query in index.php
- Add getFileVisibility(string $path): ?int — replaces raw join query in media.php
- Add getThesisBannerPath(int $id): ?string — replaces unparameterised SQL injection in
edit.php (SELECT banner_path FROM theses WHERE id = $thesisId was interpolating $thesisId
directly into the query string; now parameterised via prepared statement)
- Add getThesisRawFields(int $id): ?array — replaces raw SELECT license_id/access_type_id/
context_note in edit.php
- Add getThesisCount(): int — replaces raw SELECT COUNT(*) in system.php
Callers updated: public/tfe.php, public/index.php, public/media.php,
public/admin/edit.php, public/admin/system.php
Items resolved:
- #3 (HIGH): Move file uploads outside webroot to STORAGE_ROOT (/var/www/posterg/storage).
Uploads were previously stored in public/admin/actions/data/ which is web-accessible.
- #4 (HIGH): Align file paths and add media.php controller.
DB paths are now storage-relative (theses/YEAR/ID/file, covers/file).
New public/media.php serves files with path-traversal jail, MIME allow-list,
and proper caching headers. memoire.php and search.php updated to use /media.php?path=.
Also fixed: cover images were never recorded in thesis_files (broken INSERT).
- #5 (HIGH): RateLimit::getClientIdentifier() now uses REMOTE_ADDR only.
HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP are attacker-controlled headers that
allowed unlimited rate-limit bypass by rotating spoofed IPs.
- #6 (HIGH): Port public/admin/.htaccess security rules to nginx/posterg.conf.
Apache .htaccess directives are silently ignored by nginx; none were active.
CSP added to /admin/ location block, .log file denial added globally,
autoindex off made explicit. Documented in nginx/HTACCESS_TO_NGINX.md.
Supporting changes:
- config/bootstrap.php: add STORAGE_ROOT constant
- nginx/SECURITY_HEADERS.md: updated to reflect admin CSP and pending public CSP
- docs/TODO.SECURITY.md: items #3-6 moved to resolved; priority order updated