Upload files to AWS S3 directly from the browser using presigned POST requests, reducing server load and bandwidth usage.
- PHP 8.1 or higher
- Laravel 9.x, 10.x, or 11.x
- AWS S3 bucket with appropriate permissions
composer require hassan/laravel-s3-browser-based-uploadsFor Laravel 9+, you may need to install Flysystem dependencies:
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependenciesphp artisan vendor:publish --provider="Hassan\S3BrowserBasedUploads\ServiceProvider" --tag=configAdd your AWS settings to .env:
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-nameFor browser uploads to work, you must configure CORS on your S3 bucket. Add this CORS configuration in your AWS S3 Console:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["POST"],
"AllowedOrigins": ["https://yourdomain.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]Important: Replace https://yourdomain.com with your actual domain(s). For local development, you may add http://localhost:8000 or use ["*"] (not recommended for production).
use Hassan\S3BrowserBasedUploads\Facades\S3BrowserBasedUploads;
// Get the S3 endpoint URL
$endpointUrl = S3BrowserBasedUploads::getEndpointUrl();
// Get the presigned POST fields
$fields = S3BrowserBasedUploads::getFields();
// Use a different connection
$fields = S3BrowserBasedUploads::connection('secure_images')->getFields();const formData = new FormData();
@foreach(S3BrowserBasedUploads::getFields() as $key => $value)
formData.append('{{ $key }}', '{{ $value }}');
@endforeach
formData.append('Content-Type', file.type);
formData.append('file', file, file.name);
const request = new XMLHttpRequest();
request.open('POST', "{{ S3BrowserBasedUploads::getEndpointUrl() }}");
request.send(formData);Check out the demo with Filepond
You can optionally register a route that returns the credentials as JSON:
// In your RouteServiceProvider or routes/web.php
use Hassan\S3BrowserBasedUploads\S3BrowserBasedUploads;
public function boot()
{
// Registers GET route: /s3_browser_based_uploads/credentials
S3BrowserBasedUploads::routes();
// With custom options (e.g., authentication middleware)
S3BrowserBasedUploads::routes([
'middleware' => ['auth', 'throttle:60,1'],
'prefix' => 'api/uploads',
]);
}This creates an endpoint that returns:
{
"url": "https://your-bucket.s3.amazonaws.com",
"fields": {
"key": "tmp/images/${filename}",
"policy": "eyJ...",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": "...",
"x-amz-date": "...",
"x-amz-signature": "..."
}
}-
Filename Sanitization: Using
${filename}in your config can expose you to path traversal attacks. Consider:// In your backend before generating credentials 'key' => 'uploads/' . Str::uuid() . '.' . $extension
-
File Size Limits: Always set
content-length-rangein your config to prevent abuse:['content-length-range', 1, 10485760] // 1 byte to 10MB
-
Content-Type Validation: Restrict file types using conditions:
['starts-with', '$Content-Type', 'image/'] // Images only ['eq', '$Content-Type', 'application/pdf'] // PDFs only
-
Short Expiration Times: Use short-lived URLs (1-15 minutes recommended):
'expiration_time' => '+5 minutes'
-
Rate Limiting: The credentials endpoint includes default rate limiting (60 requests/minute). Adjust as needed.
-
HTTPS Only: Always use HTTPS in production to prevent credential interception.
-
Bucket Permissions: Set appropriate S3 bucket policies and ACLs. Avoid public write access.
Your AWS IAM user needs these S3 permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}- Does not work with AWS IAM Identity Center credentials (use standard IAM credentials)
- Maximum expiration time is capped at 12 hours for security
- Requires CORS configuration on S3 bucket
If you discover any security related issues, please email hello@hassan-ali.me instead of using the issue tracker.