Harden export-solution ZIP validation - #385
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the Power Pages export-solution validator by replacing reliance on external unzip/grep with a bounded, fail-closed ZIP parser implemented in Node.js, and adds unit tests to cover both valid and malicious/corrupt archive scenarios.
Changes:
- Implemented in-process ZIP central-directory parsing with strict bounds checks, rejecting ZIP64/multi-disk archives and verifying
Solution.xmlCRC integrity. - Added size limits (100 MiB) and switched validation to block on malformed/unsupported archives instead of silently approving when external tools are missing.
- Added a comprehensive Node test suite covering valid archives, missing/corrupt entries, duplicates, oversized zips, and shell-metacharacter filenames.
Show a summary per file
| File | Description |
|---|---|
| plugins/power-pages/skills/export-solution/scripts/validate-export.js | Replaces platform/tool-dependent archive inspection with bounded ZIP parsing and explicit fail-closed validation of Solution.xml. |
| plugins/power-pages/scripts/tests/validate-export.test.js | Adds automated coverage for valid, corrupt, missing, duplicate, oversized, and tool-independent validation cases. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
# Conflicts: # plugins/power-pages/skills/export-solution/scripts/validate-export.js
Propagate the latest #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Thanks for the hardening work here — I found one remaining gap to address before merge. Severity: Low/Medium (regression coverage gap)
The implementation appears to guard this via Suggested fix: Add a |
There was a problem hiding this comment.
Review details
Suppressed comments (2)
plugins/power-pages/skills/export-solution/scripts/validate-export.js:278
- Hardcoding "100 MiB" in the error message can drift from MAX_SOLUTION_ZIP_SIZE if the limit changes, making the message misleading. Derive the displayed MiB value from the constant instead.
if (stat.size > MAX_SOLUTION_ZIP_SIZE) {
return block(`Solution zip '${path.basename(zipPath)}' exceeds the supported 100 MiB package size.`);
}
plugins/power-pages/skills/export-solution/scripts/validate-export.js:149
- ZIP entry names are untrusted input; interpolating them directly into error messages can allow log/terminal injection (e.g., newlines/control chars) and make diagnostics hard to read. Consider escaping/quoting entry names (e.g., JSON.stringify) before including them in thrown errors.
const localHeaderEnd = entry.localHeaderOffset + 30;
if (localHeaderEnd > entry.centralDirectoryStart || localHeaderEnd > archive.length) {
throw new Error(`the local header for '${entry.name}' is truncated`);
}
if (archive.readUInt32LE(entry.localHeaderOffset) !== LOCAL_FILE_HEADER_SIGNATURE) {
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Propagate the final #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
plugins/power-pages/skills/export-solution/scripts/validate-export.js:225
validateZipContainsSolutionXml()currently only treats an entry named exactlysolution.xmlat the ZIP root as valid (entry.name.replace(/\\/g,'/').toLowerCase() === 'solution.xml'). The previousunzip -l | grep -i solution.xmlbehavior would also matchsome/path/Solution.xml, so this is a stricter behavior change that may reject otherwise-valid packages. If root-only is intended, the file header comment should say “root Solution.xml”; otherwise consider matching by basename/path-suffix.
const solutionEntries = entries.filter((entry) => {
return entry.name.replace(/\\/g, '/').toLowerCase() === 'solution.xml';
});
plugins/power-pages/skills/export-solution/scripts/validate-export.js:140
crc32()is implemented with an 8-iteration inner loop per byte. WithMAX_SOLUTION_XML_SIZEallowing up to 100 MiB, this can turn a valid-but-largeSolution.xmlinto a very slow validation step. A small precomputed 256-entry CRC table reduces this to one lookup per byte and keeps worst-case runtime bounded more reasonably.
function crc32(data) {
let crc = 0xffffffff;
for (const byte of data) {
crc ^= byte;
for (let bit = 0; bit < 8; bit++) {
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Propagate current main and the latest #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
156559b to
d0532d5
Compare
Summary
Solution.xmlintegrityTesting
POWER_PLATFORM_SKILLS_TELEMETRY_POWER_PAGES_OPTOUT=1 node --test plugins/power-pages/scripts/tests/