Summary
@tinacms/cli recently added lexical path-traversal checks to the dev media routes, but the implementation still validates only the path string and does not resolve symlink or junction targets.
If a link already exists under the media root, Tina accepts a path like pivot/written-from-media.txt as "inside" the media directory and then performs real filesystem operations through that link target. This allows out-of-root media listing and write access, and the same root cause also affects delete.
Details
The dev media handlers validate user-controlled paths with:
function resolveWithinBase(userPath: string, baseDir: string): string {
const resolvedBase = path.resolve(baseDir);
const resolved = path.resolve(path.join(baseDir, userPath));
if (resolved === resolvedBase) {
return resolvedBase;
}
if (resolved.startsWith(resolvedBase + path.sep)) {
return resolved;
}
throw new PathTraversalError(userPath);
}
function resolveStrictlyWithinBase(userPath: string, baseDir: string): string {
const resolvedBase = path.resolve(baseDir) + path.sep;
const resolved = path.resolve(path.join(baseDir, userPath));
if (!resolved.startsWith(resolvedBase)) {
throw new PathTraversalError(userPath);
}
return resolved;
}
But the validated path is then used directly for real filesystem access:
filesStr = await fs.readdir(validatedPath);
...
await fs.ensureDir(path.dirname(saveTo));
file.pipe(fs.createWriteStream(saveTo));
...
await fs.remove(file);
This does not account for symlinks/junctions already present below the media root. A path such as pivot/secret.txt can be lexically inside the media directory while the filesystem target is outside it.
Local Reproduction
I verified this locally with a real junction on Windows.
Test layout:
- media root:
D:\bugcrowd\tinacms\temp\junction-repro4\public\uploads
- junction under media root:
public\uploads\pivot -> D:\bugcrowd\tinacms\temp\junction-repro4\outside
- file outside the media root:
outside\secret.txt
Tina's current media-path validation logic was applied and used to perform the same list/write operations the route handlers use.
Observed result:
{
"media": {
"base": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads",
"resolvedListPath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot",
"listedEntries": [
"secret.txt"
],
"resolvedWritePath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot\\written-from-media.txt",
"outsideWriteExists": true,
"outsideWriteContents": "MEDIA_ESCAPE"
}
}
This shows the problem clearly:
- the path validator accepted
pivot
- listing revealed a file from outside the media root
- writing to
pivot/written-from-media.txt created outside\written-from-media.txt
The delete path uses the same flawed containment model and should be hardened at the same time.
Impact
- Out-of-root file listing via
/media/list/...
- Out-of-root file write via
/media/upload/...
- Likely out-of-root file delete via
/media/... DELETE, using the same path-validation gap
- Bypass of the recent path traversal hardening for any deployment whose media tree contains a link to another location
This is especially relevant in development and self-hosted workflows where the media directory may contain symlinks or junctions intentionally or via repository content.
Recommended Fix
Harden media path validation with canonical filesystem checks:
- resolve the real base path with
fs.realpath()
- resolve the real target path, or for writes the nearest existing parent
- compare canonical paths rather than lexical strings
- reject any operation that traverses through a symlink/junction to leave the real media root
path.resolve(...).startsWith(...) is not sufficient for filesystem security on linked paths.
Resources
packages/@tinacms/cli/src/next/commands/dev-command/server/media.ts
packages/@tinacms/cli/src/server/models/media.ts
packages/@tinacms/cli/src/utils/path.ts
References
Summary
@tinacms/clirecently added lexical path-traversal checks to the dev media routes, but the implementation still validates only the path string and does not resolve symlink or junction targets.If a link already exists under the media root, Tina accepts a path like
pivot/written-from-media.txtas "inside" the media directory and then performs real filesystem operations through that link target. This allows out-of-root media listing and write access, and the same root cause also affects delete.Details
The dev media handlers validate user-controlled paths with:
But the validated path is then used directly for real filesystem access:
This does not account for symlinks/junctions already present below the media root. A path such as
pivot/secret.txtcan be lexically inside the media directory while the filesystem target is outside it.Local Reproduction
I verified this locally with a real junction on Windows.
Test layout:
D:\bugcrowd\tinacms\temp\junction-repro4\public\uploadspublic\uploads\pivot -> D:\bugcrowd\tinacms\temp\junction-repro4\outsideoutside\secret.txtTina's current media-path validation logic was applied and used to perform the same list/write operations the route handlers use.
Observed result:
{ "media": { "base": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads", "resolvedListPath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot", "listedEntries": [ "secret.txt" ], "resolvedWritePath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\public\\uploads\\pivot\\written-from-media.txt", "outsideWriteExists": true, "outsideWriteContents": "MEDIA_ESCAPE" } }This shows the problem clearly:
pivotpivot/written-from-media.txtcreatedoutside\written-from-media.txtThe delete path uses the same flawed containment model and should be hardened at the same time.
Impact
/media/list/.../media/upload/.../media/...DELETE, using the same path-validation gapThis is especially relevant in development and self-hosted workflows where the media directory may contain symlinks or junctions intentionally or via repository content.
Recommended Fix
Harden media path validation with canonical filesystem checks:
fs.realpath()path.resolve(...).startsWith(...)is not sufficient for filesystem security on linked paths.Resources
packages/@tinacms/cli/src/next/commands/dev-command/server/media.tspackages/@tinacms/cli/src/server/models/media.tspackages/@tinacms/cli/src/utils/path.tsReferences