forked from mosaicstack/stack
fix: reject malformed generated roots
This commit is contained in:
+11
-7
@@ -164,11 +164,11 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid?
|
||||
let generated = [];
|
||||
try {
|
||||
const nextStats = await lstat(nextDir);
|
||||
if (nextStats.isSymbolicLink()) {
|
||||
if (!nextStats.isDirectory() || nextStats.isSymbolicLink()) {
|
||||
return {
|
||||
code: GENERATED_STATE_EXIT,
|
||||
message:
|
||||
'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next contains a symbolic link and is not trustworthy; run pnpm clean:generated, then rerun the gate',
|
||||
'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next must be a real directory, not a symbolic link, and is not trustworthy; run pnpm clean:generated, then rerun the gate',
|
||||
};
|
||||
}
|
||||
generated = [nextDir, ...(await entries(nextDir))];
|
||||
@@ -183,11 +183,15 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid?
|
||||
if (uid !== undefined && stats.uid !== uid) foreign.push(path.relative(root, target));
|
||||
}
|
||||
|
||||
// Threat model: this detects accidental, independent, or stale generated-state
|
||||
// mutation. It does NOT defend against an actor with same-UID write access to
|
||||
// the generated tree: that actor can regenerate both the manifest and marker
|
||||
// consistently (CWE-345). No local construction can without a trust anchor
|
||||
// outside that actor's authority.
|
||||
// Detects accidental, independent, stale, and foreign-residue mutation of
|
||||
// generated state: the class this check was born from was a five-month-stale
|
||||
// .next whose validator referenced deleted pages and produced 19 phantom TS2307
|
||||
// errors indistinguishable from real type errors.
|
||||
//
|
||||
// Does NOT defend against an actor with same-UID write access to the generated
|
||||
// tree, which can regenerate both the manifest and marker consistently
|
||||
// (CWE-345). No local construction can, absent a trust anchor outside that
|
||||
// actor's authority. RM-59 tracks executor/spine-side attestation.
|
||||
let certification = null;
|
||||
let certifiedManifest = null;
|
||||
try {
|
||||
|
||||
@@ -128,6 +128,17 @@ test('generated-state symbolic links are accepted only when exactly build-certif
|
||||
assert.match(result.message, /symbolic link/);
|
||||
});
|
||||
|
||||
await t.test('apps/web/.next is rejected when it is not a directory', async () => {
|
||||
const root = await fixture('non-directory-next-root');
|
||||
await installRequiredBins(root);
|
||||
await writeFile(path.join(root, 'apps', 'web', '.next'), 'not a Next build\n');
|
||||
|
||||
const result = await runPreflight({ root });
|
||||
assert.equal(result.code, 43);
|
||||
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
|
||||
assert.match(result.message, /real directory/);
|
||||
});
|
||||
|
||||
await t.test('an added descendant symlink is rejected', async () => {
|
||||
const root = await fixture('symbolic-next-added');
|
||||
await installRequiredBins(root);
|
||||
|
||||
Reference in New Issue
Block a user