Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/core/src/utils/trust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,67 @@ describe('Trust Utility (Core)', () => {
expect(folders.isPathTrusted(path.resolve('/other'))).toBeUndefined();
});

it('should not let a TRUST_PARENT rule override a more specific sibling rule with a shorter path', () => {
// Regression test: the "longest match wins" specificity comparison must
// use the rule's *effective* boundary (dirname(rulePath) for
// TRUST_PARENT), not the raw configured rulePath length -- otherwise an
// unrelated TRUST_PARENT rule whose own path string happens to be longer
// can silently override a more specific, explicit DO_NOT_TRUST rule that
// sits exactly at that parent boundary.
const config = {
[path.resolve('/home/victim/repos/some-long-legit-project-name')]:
TrustLevel.TRUST_PARENT,
[path.resolve('/home/victim/repos/evil')]: TrustLevel.DO_NOT_TRUST,
};
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config));

const folders = loadTrustedFolders();

expect(folders.isPathTrusted(path.resolve('/home/victim/repos/evil'))).toBe(
false,
);
expect(
folders.isPathTrusted(
path.resolve('/home/victim/repos/evil/nested/file.txt'),
),
).toBe(false);
// Sibling folders not covered by the more specific DO_NOT_TRUST rule
// should still inherit the TRUST_PARENT grant.
expect(
folders.isPathTrusted(path.resolve('/home/victim/repos/some-other-dir')),
).toBe(true);
});

it('should prefer DO_NOT_TRUST when a TRUST_PARENT rule resolves to the same boundary', () => {
// A TRUST_PARENT rule on `<dir>/.gemini` and a DO_NOT_TRUST rule on `<dir>`
// resolve to the exact same effective boundary, so neither is more
// specific than the other. The outcome must not depend on config key
// order: DO_NOT_TRUST wins either way.
const doNotTrustPath = path.resolve('/home/victim/project');
const trustParentPath = path.resolve('/home/victim/project/.gemini');

for (const config of [
{
[doNotTrustPath]: TrustLevel.DO_NOT_TRUST,
[trustParentPath]: TrustLevel.TRUST_PARENT,
},
{
[trustParentPath]: TrustLevel.TRUST_PARENT,
[doNotTrustPath]: TrustLevel.DO_NOT_TRUST,
},
]) {
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config));
resetTrustedFoldersForTesting();

const folders = loadTrustedFolders();

expect(folders.isPathTrusted(doNotTrustPath)).toBe(false);
expect(folders.isPathTrusted(path.join(doNotTrustPath, 'file.txt'))).toBe(
false,
);
}
});

it('should handle TRUST_PARENT', () => {
const config = {
[path.resolve('/project/.gemini')]: TrustLevel.TRUST_PARENT,
Expand Down
39 changes: 37 additions & 2 deletions packages/core/src/utils/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ function getRealPath(location: string): string {
return realPath;
}

/**
* Ranks rules that resolve to the same effective boundary. Higher wins:
* DO_NOT_TRUST is secure-by-default, and an explicit TRUST_FOLDER on the
* boundary itself is more deliberate than a TRUST_PARENT inherited from a rule
* one level deeper.
*/
function trustPrecedence(trustLevel: TrustLevel | undefined): number {
switch (trustLevel) {
case TrustLevel.DO_NOT_TRUST:
return 3;
case TrustLevel.TRUST_FOLDER:
return 2;
case TrustLevel.TRUST_PARENT:
return 1;
default:
return 0;
}
}

export class LoadedTrustedFolders {
constructor(
readonly user: TrustedFoldersFile,
Expand Down Expand Up @@ -171,8 +190,24 @@ export class LoadedTrustedFolders {
const normalizedEffectivePath = normalizePath(realEffectivePath);

if (isSubpath(normalizedEffectivePath, normalizedLocation)) {
if (rulePath.length > longestMatchLen) {
longestMatchLen = rulePath.length;
// Compare by the effective (post-dirname-for-TRUST_PARENT) path
// length, not the raw configured rulePath length -- a TRUST_PARENT
// rule's own path is always one segment deeper than the boundary it
// actually applies (its parent directory), so scoring it by its own
// length overstates its specificity and can let it win over a more
// specific, unrelated rule (e.g. an explicit DO_NOT_TRUST) that sits
// exactly at that parent boundary.
if (normalizedEffectivePath.length > longestMatchLen) {
longestMatchLen = normalizedEffectivePath.length;
longestMatchTrust = trustLevel;
} else if (
normalizedEffectivePath.length === longestMatchLen &&
trustPrecedence(trustLevel) > trustPrecedence(longestMatchTrust)
) {
// Distinct rules can resolve to the same boundary (e.g. DO_NOT_TRUST
// on `/a/b` and TRUST_PARENT on `/a/b/child`). Object key order must
// not decide which one applies, so break the tie deterministically
// and secure-by-default rather than leaving it to iteration order.
longestMatchTrust = trustLevel;
}
Comment thread
herdiyana256 marked this conversation as resolved.
}
Expand Down