83 lines
4.2 KiB
JavaScript
83 lines
4.2 KiB
JavaScript
'use strict';
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const utils = require('../js/spine-utils');
|
|
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-security-'));
|
|
try {
|
|
for (const unsafe of [
|
|
'../../../secret.png',
|
|
'..\\..\\secret.png',
|
|
'/tmp/secret.png',
|
|
'C:\\secret.png',
|
|
'C:secret.png',
|
|
'\\\\server\\share\\secret.png',
|
|
'file:///tmp/secret.png',
|
|
'folder/../secret.png',
|
|
'folder\\..\\secret.png',
|
|
'bad\0name.png',
|
|
]) {
|
|
assert.throws(() => utils.validateAtlasPageName(unsafe), (error) => error && error.code === 'UNSAFE_ATLAS_PATH', unsafe);
|
|
}
|
|
assert.equal(utils.validateAtlasPageName('./textures/page.png'), 'textures/page.png');
|
|
|
|
const atlasDir = path.join(root, 'atlas');
|
|
fs.mkdirSync(path.join(atlasDir, 'textures'), { recursive: true });
|
|
const nestedTexture = path.join(atlasDir, 'textures', 'page.png');
|
|
fs.writeFileSync(nestedTexture, 'texture');
|
|
assert.equal(utils.resolveAtlasTextureFile(atlasDir, 'textures/page.png'), nestedTexture);
|
|
|
|
const outside = path.join(root, 'outside.png');
|
|
const link = path.join(atlasDir, 'linked.png');
|
|
fs.writeFileSync(outside, 'private');
|
|
try {
|
|
fs.symlinkSync(outside, link);
|
|
assert.throws(() => utils.resolveAtlasTextureFile(atlasDir, 'linked.png'), (error) => error && error.code === 'UNSAFE_ATLAS_PATH');
|
|
} catch (error) {
|
|
if (!['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
}
|
|
|
|
const library = path.join(root, 'safe.library');
|
|
const images = path.join(library, 'images');
|
|
fs.mkdirSync(images, { recursive: true });
|
|
fs.writeFileSync(path.join(library, 'metadata.json'), JSON.stringify({ folders: [{ id: 'spine', name: 'Spine', children: [] }] }));
|
|
function item(id, name, contents) {
|
|
const dir = path.join(images, id + '.info');
|
|
fs.mkdirSync(dir);
|
|
fs.writeFileSync(path.join(dir, 'metadata.json'), JSON.stringify({ id, name: path.parse(name).name, ext: path.extname(name).slice(1), folders: ['spine'], isDeleted: false }));
|
|
const file = path.join(dir, name);
|
|
fs.writeFileSync(file, contents);
|
|
return file;
|
|
}
|
|
const skeleton = item('SKELETON', 'hero.skel', Buffer.from('\0' + '3.8.1\0'));
|
|
item('ATLAS', 'hero.atlas', 'textures/hero.png\nsize: 64,64\n');
|
|
const texture = item('TEXTURE', 'hero.png', 'texture');
|
|
const assets = utils.discoverAssets(skeleton, { strictStem: true, refreshIndex: true });
|
|
assert.equal(assets.texturePaths['textures/hero.png'], texture, 'cross-item texture must come from a validated Eagle item');
|
|
|
|
const maliciousDir = path.join(root, 'malicious');
|
|
fs.mkdirSync(maliciousDir);
|
|
const maliciousSkeleton = path.join(maliciousDir, 'evil.skel');
|
|
fs.writeFileSync(maliciousSkeleton, Buffer.from('\0' + '3.8.1\0'));
|
|
fs.writeFileSync(path.join(maliciousDir, 'evil.atlas'), '../../../secret.png\nsize: 64,64\n');
|
|
assert.throws(() => utils.discoverAssets(maliciousSkeleton), (error) => error && error.code === 'UNSAFE_ATLAS_PATH');
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../manifest.json'), 'utf8'));
|
|
const preview = Object.values(manifest.preview || {});
|
|
assert.equal(preview.some((entry) => entry && entry.thumbnail), false, 'import-time thumbnail handler must stay disabled');
|
|
assert.equal(fs.existsSync(path.join(__dirname, '../thumbnail/spine.js')), false);
|
|
assert.equal(fs.existsSync(path.join(__dirname, '../js/spine-reconcile-worker.js')), false);
|
|
const viewerSource = fs.readFileSync(path.join(__dirname, '../viewer/spine.html'), 'utf8');
|
|
assert.match(viewerSource, /currentItem\.data\.customThumbnail === true && !window\.confirm/);
|
|
assert.match(viewerSource, /activeAssets\.skeletonPath[\s\S]*activeAssets\.atlasPath[\s\S]*Object\.values\(activeAssets\.texturePaths/);
|
|
assert.match(viewerSource, /fresh\.data\.customThumbnail === true\) \{ skipped\+\+; continue; \}[\s\S]*setEagleCustomThumbnail\(fresh\.id/);
|
|
assert.match(viewerSource, /groupThumbBtn[\s\S]*thumbBtn/);
|
|
assert.match(viewerSource, /path\.join\(os\.tmpdir\(\), 'spineviewer-current-'/);
|
|
assert.doesNotMatch(viewerSource, /fs\.writeFileSync\(outPath/);
|
|
console.log('PASS: atlas path boundaries, validated Eagle pairing, no import thumbnail handler, and manual current-item confirmation');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|