diff --git a/scripts/validate-architecture-assets.mjs b/scripts/validate-architecture-assets.mjs
index 9a197cdd..cc90a82e 100644
--- a/scripts/validate-architecture-assets.mjs
+++ b/scripts/validate-architecture-assets.mjs
@@ -50,7 +50,9 @@ const SITE_CHROME_EXTENSIONS = new Set([...ALLOWED_ASSET_EXTENSIONS, '.ico']);
// favicons - rather than imported assets, but they are served from the same
// origin as everything else, so they carry the same security gate. static/img
// is walked shallowly because its image subdirectories are listed above, each
-// with its own quality setting.
+// with its own quality setting; walk() rejects any *other* subdirectory it
+// finds there, since a shallow walk would otherwise publish its contents with
+// no gate at all.
//
// static/fonts is deliberately outside the gate (it holds no SVG, and its
// extensions are legitimately outside the image allow-list) via the
@@ -165,7 +167,23 @@ function walk(dir, recurse = true) {
record(path, 'error', 'is a symbolic link; symlinks are not allowed');
return [];
}
- if (entry.isDirectory()) return recurse ? walk(path) : [];
+ if (entry.isDirectory()) {
+ if (recurse) return walk(path);
+ // A shallow walk covers only the files sitting directly in the
+ // directory, so every subdirectory below it must be an asset root with
+ // its own assetDirs entry (returned above via assetRootPaths). One that
+ // is not is published at the site origin with no gate at all -- the same
+ // silent gap checkForUngatedStaticEntries() closes at the static/ root,
+ // one level down.
+ record(
+ path,
+ 'error',
+ 'is a subdirectory of a shallowly walked asset directory and is not ' +
+ 'covered by the asset security gate; add it to assetDirs in ' +
+ 'scripts/validate-architecture-assets.mjs',
+ );
+ return [];
+ }
return [path];
});
}
diff --git a/tests/validate-architecture-assets.test.mjs b/tests/validate-architecture-assets.test.mjs
index 9e7235fc..8e3e72b7 100644
--- a/tests/validate-architecture-assets.test.mjs
+++ b/tests/validate-architecture-assets.test.mjs
@@ -524,16 +524,44 @@ test('accepts an allowed image sitting directly in static/', () => {
assert.equal(result.status, 0, result.stderr);
});
-test('does not descend into subdirectories of the shallow static/img walk', () => {
+test('rejects a subdirectory of the shallow static/img walk', () => {
// static/img is walked with recurse: false because it holds site chrome
- // sitting directly in the directory. Its subdirectories are either asset
- // roots with their own entry or -- as here -- out of the gate's reach, so
- // an asset nested inside one is neither validated nor counted.
+ // sitting directly in the directory. A shallow walk covers only those
+ // files, so every subdirectory below it must be an asset root with its own
+ // assetDirs entry; one that is not would otherwise ship to the site origin
+ // with no gate at all.
const result = runScriptWithFixtures(SCRIPT, {
'static/img/architectures/example/diagram.svg': VALID_SVG,
'static/img/illustrations/nested.svg': '',
});
+ assert.equal(result.status, 1);
+ assert.match(
+ result.stderr,
+ /static\/img\/illustrations: is a subdirectory of a shallowly walked asset directory and is not covered by the asset security gate/,
+ );
+});
+
+test('rejects markup in a subdirectory of the shallow static/img walk', () => {
+ // The extension allow-list is the only thing that keeps a .html off the
+ // site origin, and it never ran below static/img: the file was published
+ // verbatim with the validator, the unit suite and the build all green.
+ const result = runScriptWithFixtures(SCRIPT, {
+ 'static/img/architectures/example/diagram.svg': VALID_SVG,
+ 'static/img/blog/pwn.html': '',
+ });
+ assert.equal(result.status, 1);
+ assert.match(result.stderr, /static\/img\/blog: is a subdirectory/);
+});
+
+test('does not flag the static/img subdirectories that are asset roots', () => {
+ // The three real subdirectories each have their own assetDirs entry, so
+ // they are returned by assetRootPaths before the new subdirectory check.
+ const result = runScriptWithFixtures(SCRIPT, {
+ 'static/img/architectures/example/diagram.svg': VALID_SVG,
+ 'static/img/cncf-projects/helm-helm-icon-color.svg': VALID_SVG,
+ 'static/img/awards/example.svg': VALID_SVG,
+ 'static/img/cncf_logo_white.svg': VALID_SVG,
+ });
assert.equal(result.status, 0, result.stderr);
- assert.match(result.stdout, /Validated 1 architecture asset/);
- assert.doesNotMatch(result.stderr, /illustrations/);
+ assert.match(result.stdout, /Validated 4 architecture asset/);
});