A lightweight, dependency‑free PHP application for accepting public file uploads and reviewing them through a protected admin dashboard. Built for PHP 8.3, it uses flat JSON metadata (no database) and a strict, JavaScript‑free security model.
- Public upload page (
index.php) — email‑gated uploads with extension whitelist, MIME verification, randomized server filenames, and double‑extension script blocking. - Secure admin dashboard (
admin.php) — login‑protected review of uploaded files with search, type/status/date filters, pagination, and per‑file approval. - Soft delete — files are moved to
uploads/_deleted/and flagged in metadata instead of being permanently removed; the active dashboard hides them and shows a deleted count. - Protected file serving — uploads are never served directly from the web; the admin views/downloads them through
admin.phpwith safe inline rendering for images/PDF/TXT and forced download for everything else. - No JavaScript required — the delete confirmation modal is pure CSS (
:target), so a strictscript-srcContent‑Security‑Policy can be enforced. - Mobile‑responsive admin UI — single‑column layout, top‑right navigation bar with Logout, and a table that collapses into cards on small screens.
| Area | Choice |
|---|---|
| Language | PHP 8.3 |
| Storage | Flat JSON (uploads/upload_data.json) — no DB |
| Frontend | Server‑rendered HTML + inline CSS, no JS framework |
| Web server | Apache / LiteSpeed (.htaccess) — Nginx supported with config (see below) |
| Dependencies | None |
files/
├── index.php # Public upload page
├── admin.php # Admin dashboard (login, review, approve, soft delete)
├── logo.svg # Brand asset
├── README.md # This file
└── uploads/
├── .htaccess # Denies direct web access to the whole folder
├── index.html # Empty index (anti directory-listing)
├── upload_data.json # File metadata (server-side only — never web-served)
└── _deleted/
├── .htaccess # Denies direct web access to soft-deleted files
└── index.html # Empty index
The web server must be able to write to
uploads/anduploads/_deleted/.
- PHP 8.3 (CLI/FPM/module all fine)
- Apache or LiteSpeed with
mod_headersenabled andAllowOverridepermitting.htaccess, or Nginx with the equivalent rules below - A writable
uploads/directory (anduploads/_deleted/)
- Copy the contents of
files/into your web root (or a subdirectory). - Ensure the upload directories are writable by the web server user:
chmod 755 uploads uploads/_deleted
- Confirm the
.htaccessfiles are present inuploads/anduploads/_deleted/. - Set your admin credentials (see Configuration).
- Visit
admin.phpto log in, andindex.phpfor the public upload form.
Credentials are defined near the top of admin.php:
$adminUser = "your-admin-username"; // <-- replace
$adminPassHash = "your-bcrypt-password-hash"; // <-- replace (see below)Do not commit real credentials. Keep the username and password hash out of version control (use environment variables or a local, git‑ignored config include in production).
Generate a bcrypt hash for your chosen password:
php -r 'echo password_hash("your-secure-password", PASSWORD_BCRYPT, ["cost" => 12]), PHP_EOL;'Paste the resulting hash into $adminPassHash. The app verifies logins with
password_verify(), so the plaintext password is never stored.
Defined in index.php:
$maxSize = 20 * 1024 * 1024; // 20 MB
$allowedExtensions = ["jpg","jpeg","png","gif","webp","pdf","doc","docx","zip","txt"];Adjust as needed; MIME types are validated against an allow‑list per extension.
- Session hardening —
HttpOnly,SameSite=Lax, andSecure(when served over HTTPS) cookies; session ID regenerated on login. - CSRF protection — all state‑changing POST actions (login, approvals, soft delete) require a per‑session token compared with
hash_equals(). - Output escaping — all dynamic output passes through
htmlspecialchars(). - Path‑traversal protection — requested filenames are reduced to
basename()and resolved withrealpath(), then confirmed to live inside the uploads directory. - No executable serving — script/markup extensions (
.php,.phtml,.phar,.html,.js,.svg,.json, etc.) are blocked from the view/download endpoint. - Security headers —
X-Frame-Options,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy, and a strictContent-Security-Policy(admin usesscript-src 'none'). - Direct‑access denial — the
uploads/folder denies all direct web requests; the metadata fileupload_data.jsonis additionally protected by an explicitFilesMatchrule. It is read only server‑side by PHP.
uploads/.htaccess applies a default deny plus an explicit rule for sensitive
file types (works on Apache 2.4/LiteSpeed and Apache 2.2):
Options -Indexes
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
<FilesMatch "(?i)\.(json|php|phtml|php[0-9]|phar|html?|js|svg|cgi|pl|py|sh|bash)$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>
</FilesMatch>A .htaccess only works if the server honors it. If the metadata file is
still reachable in a browser:
- Apache — ensure
AllowOverrideis notNonefor the directory:<Directory "/path/to/files/uploads"> AllowOverride All </Directory>
- Nginx (ignores
.htaccess) — add to your server block:location ^~ /files/uploads/ { deny all; return 403; }
Verify (while logged out):
curl -I https://your-domain.example/files/uploads/upload_data.json
# Expect: HTTP/1.1 403 Forbidden- Go to
admin.phpand log in. - Use Search & Filters to find files by name/email, type, approval status, or date range.
- Toggle Approve checkboxes and click Save Approval Changes to update statuses.
- Use the row actions to View (inline for image/PDF/TXT), Download, or Soft Delete a file.
- Soft‑deleted files move to
uploads/_deleted/and are counted under Deleted. - Logout is in the top‑right navigation bar.
Run locally with PHP's built‑in server:
php -S 127.0.0.1:8000 -t files
# Public upload: http://127.0.0.1:8000/index.php
# Admin: http://127.0.0.1:8000/admin.phpThe built‑in server does not process
.htaccess. Test direct‑access protection on a real Apache/LiteSpeed/Nginx environment.
Lint before committing:
php -l files/admin.php
php -l files/index.php- Admin UI update — removed the sidebar; moved Logout to a top‑right navigation bar; made the dashboard fully mobile‑responsive (no backend/logic changes).
- Security enhancement — hardened
uploads/.htaccesswith layered deny rules and an explicitFilesMatchforupload_data.json.
Add your license here (e.g. MIT). Replace this section before publishing.