Unlimited cloud storage
A simple trick to demonstrate a problem
Recently, while I was building a project, I needed a cloud storage provider to store images, videos, etc. β basically a SaaS product. I had a βΉ0 budget for the project, so I needed a service with a free tier. Luckily, there are lots of them out there…
Here is a comparison of various providers
| Provider | Free Storage | Free Bandwidth | File Size Limit | Notable Limits / Notes |
|---|---|---|---|---|
| Amazon S3 | 5 GB | 15 GB/month | 5 GB | 20,000 GET & 2,000 PUT requests/month |
| Cloudinary | 25 Credits/month | 25 GB bandwidth/month | 100 MB (images, videos) | 25 GB total storage included |
| Firebase Storage | 1 GB | 5 GB/month | 2 GB | 50K read, 20K write operations/day |
| Supabase | 1 GB | 2 GB/month | ~50 MB (browser upload) | File size capped by browser or CDN |
| ImageKit | 20 GB | 20 GB/month | 25 MB (browser), 100 MB (API) | 500 transformations/day limit |
| Uploadcare | 1.5 GB | 1 GB/month | 100 MB | 300 file uploads/month |
| Backblaze B2 | 10 GB | 1 GB/day download | No hard limit | 2,500 downloads/day |
I decided to use Cloudinary for my project because it offered a decent free tier with generous bandwidth and transformation limits. But then I noticed something interesting β all you need to sign up is a Gmail address.
So… why not just create multiple Gmail accounts and keep rotating them? Each new account gives you another free tier allowance. You can imagine the potential: 10 Gmail accounts β 10 Cloudinary accounts β 10Γ the free limit. For a mid-scale application, this setup could be sufficient.
Here is the logic:
flowchart LR
A[User uploads image] --> B[API receives image]
B --> C{Select Cloudinary account}
C -->|1st Upload| D1[Cloudinary Account 1]
C -->|2nd Upload| D2[Cloudinary Account 2]
C -->|3rd Upload| D3[Cloudinary Account 3]
C -->|4th Upload| D1
C -->|5th Upload| D2
C -->|...| E[Repeat in Round-Robin]
D1 --> F1[Upload Success]
D2 --> F2[Upload Success]
D3 --> F3[Upload Success]
I created a demo application using Node.js and Express to demonstrate this (I do not intend to use it in production β and you shouldn’t either β it’s just for educational purposes):
The code can be found in my github repo abuse-the-free-tier
Code Explanation #
Folder Structure:
.
βββ uploads/ # Temp directory for incoming files before upload
βββ views/
β βββ upload.ejs # HTML template for upload form
βββ cloudinaryAccounts.example.json # Example credentials file
βββ cloudinaryAccounts.json # Actual Cloudinary account credentials (add your accounts)
βββ cloudinary.js # Upload logic with account rotation
βββ index.js # Main server logic (Express)
βββ package.json # Node project dependencies and scripts
βββ package-lock.json # Dependency lock file
βββ README.md # Project descriptionI will just explain the main parts here :
The cloudinaryAccounts.json contains the api keys and secrets and that is not safe but its for demonstration purposes , we can easily make them as environment variables and inject them using packages like dotenv.
cloudinary.js β Round-Robin Upload Logic #
Handles:
- Account rotation via
currentIndex % accounts.length - Configuring Cloudinary SDK per request
- Uploading file to the selected Cloudinary account
const cloudinary = require("cloudinary").v2;
const accounts = require("./cloudinaryAccounts.json");Weβre importing Cloudinaryβs SDK to interact with their API. Also importing our cloudinaryAccounts.json file which contains all the account credentials.
Before this works, make sure to install Cloudinary using:
npm install cloudinary
let currentIndex = 0;
function getNextAccount() {
const account = accounts[currentIndex % accounts.length];
currentIndex++;
return account;
}Weβre using a simple trick called modulo (%) to loop through the accounts in a round-robin fashion. currentIndex % accounts.length ensures we never go out of bounds. Each time we upload, currentIndex++ moves us to the next account. Once it reaches the end, it circles back to the first one.
async function uploadImage(filePath) {
const account = getNextAccount();
cloudinary.config(account);
try {
const result = await cloudinary.uploader.upload(filePath);
return result;
} catch (error) {
console.error("Upload failed:", error);
throw error;
}
}This is a example upload function:
- Gets the next account using our round-robin method.
- Sets that account as the active Cloudinary config.
- Uploads the file at filePath.
- Returns the Cloudinary response with url, public_id, etc.
module.exports = uploadImage;We export uploadImage so we can use it in other files (like index.js).
Conclusion #
This is just for cloud object storage , most of the SaaS products can be exploited in this way , even anything that uses API keys and has a free tier. So Is there a way to prevent this ? so that software companies can make more money .