Deploy autonomous AI agents that reason, exploit, and validate complex vulnerability chains — not another scanner, an agentic system that thinks like a senior pentester.
CVE-2026-34604 is a low severity vulnerability with a CVSS score of 0.0. No known exploits currently, and patches are available.
Very low probability of exploitation
EPSS predicts the probability of exploitation in the next 30 days based on real-world threat data, complementing CVSS severity scores with actual risk assessment.
@tinacms/graphql uses string-based path containment checks in FilesystemBridge:
path.resolve(path.join(baseDir, filepath))startsWith(resolvedBase + path.sep)That blocks plain ../ traversal, but it does not resolve symlink or junction targets. If a symlink/junction already exists under the allowed content root, a path like content/posts/pivot/owned.md is still considered "inside" the base even though the real filesystem target can be outside it.
As a result, FilesystemBridge.get(), put(), delete(), and glob() can operate on files outside the intended root.
The current bridge validation is:
function assertWithinBase(filepath: string, baseDir: string): string {
const resolvedBase = path.resolve(baseDir);
const resolved = path.resolve(path.join(baseDir, filepath));
if (
resolved !== resolvedBase &&
!resolved.startsWith(resolvedBase + path.sep)
) {
throw new Error(
`Path traversal detected: "${filepath}" escapes the base directory`
);
}
return resolved;
}
But the bridge then performs real filesystem I/O on the resulting path:
public async get(filepath: string) {
const resolved = assertWithinBase(filepath, this.outputPath);
return (await fs.readFile(resolved)).toString();
}
public async put(filepath: string, data: string, basePathOverride?: string) {
const basePath = basePathOverride || this.outputPath;
const resolved = assertWithinBase(filepath, basePath);
await fs.outputFile(resolved, data);
}
public async delete(filepath: string) {
const resolved = assertWithinBase(filepath, this.outputPath);
await fs.remove(resolved);
}
This is a classic realpath gap:
This is reachable from Tina's GraphQL/local database flow. The resolver builds a validated path from user-controlled , but that validation is also string-based:
Please cite this page when referencing data from Strobes VI. Proper attribution helps support our vulnerability intelligence research.
relativePathconst realPath = path.join(collection.path, relativePath);
this.validatePath(realPath, collection, relativePath);
Database write and delete operations then call the bridge:
await this.bridge.put(normalizedPath, stringifiedFile);
...
await this.bridge.delete(normalizedPath);
This was verified llocally with a real junction on Windows, which exercises the same failure mode as a symlink on Unix-like systems.
Test layout:
D:\bugcrowd\tinacms\temp\junction-repro4content/postscontent/posts/pivot -> D:\bugcrowd\tinacms\temp\junction-repro4\outsideoutside\secret.txtTina's current path-validation logic was applied and used to perform bridge-style read/write operations through the junction.
Observed result:
{
"graphqlBridge": {
"collectionPath": "content/posts",
"requestedRelativePath": "pivot/owned.md",
"validatedRealPath": "content\\posts\\pivot\\owned.md",
"bridgeResolvedPath": "D:\\bugcrowd\\tinacms\\temp\\junction-repro4\\content\\posts\\pivot\\owned.md",
"bridgeRead": "TOP_SECRET_FROM_OUTSIDE\\r\\n",
"outsideGraphqlWriteExists": true,
"outsideGraphqlWriteContents": "GRAPHQL_ESCAPE"
}
}
That is the critical point:
content/postsoutside\secret.txtoutside\owned.mdSo the current containment check does not actually constrain filesystem access to the configured content root once a link exists inside that tree.
assertWithinBase() gap in delete()The exact network exploitability depends on how the application exposes Tina's GraphQL/content operations, but the underlying bridge bug is real and independently security-relevant.
The containment check needs to compare canonical filesystem paths, not just string-normalized paths.
For example:
fs.realpath()fs.realpath()In short: use realpath-aware containment checks for every filesystem sink, not path.resolve(...).startsWith(...) alone.
packages/@tinacms/graphql/src/database/bridge/filesystem.tspackages/@tinacms/graphql/src/database/index.tspackages/@tinacms/graphql/src/resolver/index.ts