- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
File Upload Security Mistakes in PHP Apps (and the Safe Laravel Way)
About Post
A file upload form is the only place in most apps where you let a stranger put a file of their choosing onto your server. Read that sentence again, slowly. It's a strange thing to allow, and yet almost every app does it: profile photos, ID documents, signed contracts, maintenance request pictures.
Uploads are also one of the oldest and most reliable ways into a PHP application, because the mistakes are easy to make and the code that makes them looks perfectly reasonable. Here are the classic ones, why each is dangerous, and the Laravel approach that avoids all of them.
The upload handler that does everything wrong
Here's a pattern that still shows up in older PHP code and quick tutorials:
// Don't do this
$name = $_FILES['photo']['name'];
if ($_FILES['photo']['type'] === 'image/jpeg') {
move_uploaded_file(
$_FILES['photo']['tmp_name'],
__DIR__ . "/uploads/{$name}"
);
}
It checks the type. It uses a dedicated upload function. It looks careful. It contains at least four of the mistakes below.
Mistake 1: trusting the extension or the browser's MIME type
Why it's dangerous: $_FILES['photo']['type'] is sent by the client. The browser fills it in honestly, but an attacker using curl or a script can set it to anything. Upload shell.php with a type of image/jpeg and the check above passes. The file name and extension are just as easy to fake.
The fix: determine the type on the server, from the file's content. In plain PHP that's finfo. In Laravel, the mimes and mimetypes rules do it for you by inspecting the file. Combine that with an allowlist of extensions, so both the content and the name have to agree.
Even content checks aren't bulletproof: a file can be a valid image and contain hidden data. For images, re-encoding them (resizing or re-saving with GD or an image library) produces a clean file and strips metadata as a bonus.
Mistake 2: storing uploads inside the web root
Why it's dangerous: anything in your public directory can be requested by URL. If a .php file gets past your checks and lands in /uploads/, visiting /uploads/shell.php runs it. That's remote code execution: game over for that server. Some web server configurations will even execute files with double extensions like photo.php.jpg.
The fix: store uploads outside the web root, or in private object storage like S3, and serve them through a controller that checks permissions. Then even if a malicious file gets stored, there's no URL that executes it.
In Laravel, the public disk is linked into public/storage, so it's for files you genuinely want public (like a blog image). Documents, IDs and contracts belong on a private disk. In new Laravel 11 and 12 apps, the local disk points to storage/app/private for exactly this reason.
Mistake 3: keeping the user's file name
Why it's dangerous: the name is user input. Two users uploading contract.pdf overwrite each other. A name containing special characters breaks things in creative ways. And as soon as any part of a storage path comes from the request (a folder parameter, a file name in a download URL), you're open to path traversal: ../../.env is a valid string, and a careless download endpoint will happily serve it.
// Path traversal waiting to happen
return Storage::download('uploads/' . $request->query('file'));
The fix: generate the stored name yourself. Laravel's store() method gives every file a random name automatically. Keep the original name in the database for display and downloads, never as part of the path. And never build file paths from request input; look up a database record and use the path stored on it.
Mistake 4: no size limits (or limits in only one place)
Why it's dangerous: without limits, anyone can fill your disk or tie up your workers with huge uploads. Images have a sneakier version: a small file that decodes into an enormous number of pixels, using huge amounts of memory when you try to resize it.
The fix: limits exist at several layers, and they all need to agree:
- Web server:
client_max_body_sizein Nginx. - PHP:
upload_max_filesizeandpost_max_sizeinphp.ini. - Application: Laravel's
maxrule (in kilobytes for files), plus thedimensionsrule for images.
The application rule is the one that gives users a friendly error message. The others are the hard stop.
Mistake 5: serving uploads inline, from your own domain
Why it's dangerous: not every dangerous upload is a PHP file. An SVG or HTML file can contain JavaScript. If you serve it from your main domain with its natural content type, the browser runs that script in your users' sessions: stored cross-site scripting.
The fix: don't allow SVG or HTML unless you really need them. Serve user files as downloads (Content-Disposition: attachment), send X-Content-Type-Options: nosniff, and for apps with lots of user content, consider serving files from a separate domain or via signed S3 URLs.
The mindset: treat every uploaded file as hostile until proven otherwise. Validate the content, rename it, store it where it can't execute, and hand it back only to people who are allowed to see it.
The safe approach in Laravel
Here's what a careful upload looks like for, say, a signed contract document. Simplified, but every line addresses one of the mistakes above:
$request->validate([
'document' => [
'required', 'file',
'mimes:pdf,jpg,png', // content-based type check
'extensions:pdf,jpg,png', // and the name must agree
'max:10240', // 10 MB, in kilobytes
],
]);
$file = $request->file('document');
$contract->documents()->create([
'path' => $file->store("contracts/{$contract->id}", 'local'), // random name, private disk
'original_name' => $file->getClientOriginalName(), // for display only
]);
And the download goes through a controller that checks access first:
public function download(Contract $contract, Document $document)
{
Gate::authorize('view', $contract);
abort_unless($document->contract_id === $contract->id, 404);
return Storage::disk('local')
->download($document->path, $document->original_name);
}
That second check matters. Without it, a user who can see their own contract could swap the document ID in the URL and download someone else's file. It's a different bug (an insecure direct object reference), but it travels with uploads so often that it belongs on this list.
For files on S3, Storage::temporaryUrl() gives the user a short-lived signed link instead of streaming the file through PHP. The Laravel filesystem docs cover the disks and options.
The checklist
- ✅ Type checked from content, with an extension allowlist.
- ✅ Stored outside the web root or in private storage.
- ✅ Random stored names; original name kept only in the database.
- ✅ No file paths built from request input.
- ✅ Size limits at the web server, PHP and app level.
- ✅ Served as a download, after an authorization check.
- ✅ Images re-encoded; SVG and HTML rejected unless truly needed.
Which of these do you see most often in code reviews? In my experience it's storing everything on the public disk, because it's the easiest way to make the image show up.

Be first to comment it...