Harden Power Pages shell and URL handling - #386
Conversation
- Keep runtime command approvals enabled by default - Explain credential scope and narrow script allow rules Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- pin the reviewed Playwright MCP runtime package to 0.0.78\n- invoke npx through Node with raw argv and no command shell\n- cover Windows paths, spawn failures, and exit propagation\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- require an absolute host-provided plugin root\n- canonicalize and contain the launcher path\n- cover invalid roots, malicious cwd, and supported hosts\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…eadme-safety-guidance' into users/priyanshu92/fix-power-pages-mcp-root
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace dynamic shell commands with argv-based process execution and validate Microsoft service destinations before token acquisition or authenticated requests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens the Power Pages plugin’s shell execution and URL handling by replacing string-based CLI invocation with argv arrays (shell: false) and by centralizing an allowlist-based trust policy for token resources and authenticated requests across Dataverse, Power Platform, BAP, and Flow endpoints.
Changes:
- Introduces shared Microsoft-cloud URL validation helpers (HTTPS-only, no credentials/ports/fragments, strict host allowlists) and applies them to token acquisition and authenticated requests.
- Reworks
az/pacexecution in affected paths to useexecFileSyncwith argument arrays andshell: false, reducing command-injection risk. - Expands unit/integration coverage for metacharacters, unsafe destinations, sovereign cloud hosts, and BAP polling redirect defenses (including injectable local-only request transport for integration tests).
Show a summary per file
| File | Description |
|---|---|
| plugins/power-pages/scripts/lib/validation-helpers.js | Adds centralized URL trust policy + switches az token acquisition to execFileSync with validated resource URLs. |
| plugins/power-pages/scripts/lib/list-tenant-envs.js | Validates BAP base URLs and Dataverse origins before token acquisition / probing. |
| plugins/power-pages/scripts/lib/install-pipelines-app.js | Hardens BAP URL handling, validates Location redirects, and switches PAC fallback to argv arrays with shell: false. |
| plugins/power-pages/scripts/lib/fix-blocked-attachments.js | Switches PAC invocation to argv arrays (execFileSync, shell: false) and validates environment URLs. |
| plugins/power-pages/scripts/lib/ensure-pipelines-host-detect.js | Validates Dataverse-derived URLs before token acquisition and host probing. |
| plugins/power-pages/scripts/lib/estimate-solution-size.js | Allows injecting a request transport for integration testing while preserving default behavior. |
| plugins/power-pages/scripts/lib/check-solution-installed.js | Replaces local URL sanitization with the shared trust policy wrapper. |
| plugins/power-pages/scripts/tests/validation-helpers.test.js | Updates az invocation assertions for argv arrays + adds URL/host/metacharacter validation coverage. |
| plugins/power-pages/scripts/tests/list-tenant-envs.test.js | Adds coverage for sovereign hosts and malicious Dataverse-derived API host rejection. |
| plugins/power-pages/scripts/tests/install-pipelines-app.test.js | Adds tests for PAC argv execution and BAP Location redirect rejection. |
| plugins/power-pages/scripts/tests/fix-blocked-attachments.test.js | Adds tests ensuring argv literal handling, shell: false, and unsafe env URL rejection. |
| plugins/power-pages/scripts/tests/ensure-pipelines-host-detect.test.js | Adds regression test rejecting Dataverse-derived host URLs outside the allowlist. |
| plugins/power-pages/scripts/tests/check-solution-installed.test.js | Updates expectations to match stricter rejection of paths/queries/fragments/ports/control chars. |
| plugins/power-pages/scripts/tests/integration/mock-dataverse.js | Adds a local-only HTTP transport used by integration tests. |
| plugins/power-pages/scripts/tests/integration/discover-integration.test.js | Injects local-only transport to keep integration coverage while production rejects HTTP/non-allowlisted hosts. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
Simplify origin validation and cover canonical hostname normalization. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d056b46b-d5ce-487b-b4f9-74115a21eed3
- stub only npx-cli existence probes in the MCP bootstrap preload\n- assert the deterministic npm-resolution seam is exercised\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Review details
Suppressed comments (1)
plugins/power-pages/scripts/lib/validation-helpers.js:237
parseTrustedMicrosoftUrlrequires the input string to start with lowercasehttps://. URL schemes are case-insensitive, so values likeHTTPS://org.crm.dynamics.comwill be rejected even though they parse as HTTPS. Since the port check is the only reason to inspect the raw string, this should be made case-insensitive rather than enforcing a canonical prefix.
if (!value.startsWith('https://')) {
throw new Error(`${purpose} must use the canonical "https://" scheme.`);
}
const authority = value.slice('https://'.length).split(/[/?#]/, 1)[0];
if (authority.includes(':')) {
throw new Error(`${purpose} must not contain a port.`);
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
- resolve the default npx CLI path inside launch error handling\n- report missing npm once without spawning\n- preserve explicit path injection and shell-free exact-pin behavior\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep raw authority port checks while returning canonical trusted Microsoft URLs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d056b46b-d5ce-487b-b4f9-74115a21eed3
Use argv-based execution in activation and export validators and cover metacharacter paths through the PostToolUse hook. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d056b46b-d5ce-487b-b4f9-74115a21eed3
Use forward slashes in NODE_OPTIONS preload paths so Windows preserves absolute module paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d056b46b-d5ce-487b-b4f9-74115a21eed3
There was a problem hiding this comment.
Review details
Suppressed comments (2)
plugins/power-pages/scripts/lib/ensure-pipelines-host-detect.js:232
- These assignments store env.instanceUrl/env.instanceApiUrl after validating them, but they keep the un-normalized original strings (case/trailing slash). Since validateDataverseEnvironmentUrl() returns the canonical origin, store the canonical values so cache/output comparisons stay consistent across inputs.
helpers.validateDataverseEnvironmentUrl(env.instanceUrl, 'Resolved host environment URL');
helpers.validateDataverseEnvironmentUrl(env.instanceApiUrl, 'Resolved host API URL');
baseOut.finalHostEnvUrl = env.instanceUrl;
baseOut.finalHostEnvName = env.displayName || null;
baseOut.finalHostInstanceApiUrl = env.instanceApiUrl;
plugins/power-pages/scripts/lib/ensure-pipelines-host-detect.js:90
- originOf() validates the Dataverse origin but then reconstructs and returns it from URL pieces. This duplicates normalization logic and risks drifting from validateDataverseEnvironmentUrl() (which already canonicalizes the origin). Return the validated canonical origin directly.
This issue also appears on line 228 of the same file.
helpers.validateDataverseEnvironmentUrl(u.origin);
return `${u.protocol}//${u.host}`;
- Files reviewed: 18/18 changed files
- Comments generated: 0 new
- Review effort level: Lite
Store validated Dataverse origins for live, discovered, and cached pipeline hosts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d056b46b-d5ce-487b-b4f9-74115a21eed3
There was a problem hiding this comment.
Review details
Suppressed comments (3)
plugins/power-pages/scripts/lib/install-pipelines-app.js:234
tryPacFallbackcalls.toLowerCase()on(err.stderr || err.message || ''). Depending on howexecImplthrows (or Node behavior across versions),err.stderrcan be a Buffer, which would make.toLowerCase()throw and mask the original PAC failure. Coerce to string first to keep the fallback robust.
} catch (err) {
lastErr = err;
// Try next candidate if PAC reports an unrecognized arg / subcommand.
const stderr = (err.stderr || err.message || '').toLowerCase();
if (!/unrecognized|unknown|invalid argument/i.test(stderr)) break;
plugins/power-pages/skills/export-solution/scripts/validate-export.js:52
- The unzip inspection is wrapped in a broad catch that ignores all failures. That means a corrupt ZIP (unzip returns non-zero) will be treated the same as “unzip not installed”, and the validator will silently fall back to the size-only check, potentially approving a corrupt export. Consider only suppressing ENOENT (unzip missing) and blocking on other unzip errors so corrupted exports are caught when unzip is available.
const output = execFileSync('unzip', ['-l', zipPath], {
encoding: 'utf8',
timeout: 10000,
stdio: ['ignore', 'pipe', 'ignore'],
shell: false,
plugins/power-pages/scripts/lib/ensure-pipelines-host-detect.js:89
originOfvalidates the URL viavalidateDataverseEnvironmentUrl(u.origin)but then discards the canonicalized return value and reconstructs an origin string fromu.protocol/u.host. Returning the validator’s output directly keeps normalization consistent with other call sites (e.g., plugins/power-pages/scripts/lib/list-tenant-envs.js:186-191) and avoids duplicated origin formatting logic.
function originOf(url) {
try {
const trustedUrl = helpers.validateAuthenticatedRequestUrl(url);
const u = new URL(trustedUrl);
return helpers.validateDataverseEnvironmentUrl(u.origin, 'Dataverse URL origin');
- Files reviewed: 18/18 changed files
- Comments generated: 1
- Review effort level: Lite
Propagate the latest #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Great security pass overall. I found two medium-priority issues to fix before merge. Severity: Medium
Suggested fix:
|
- Guard EACCES/EPERM errors while statting the declared plugin root. - Guard EACCES/EPERM errors while statting the resolved launcher. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e799af6-1da3-4859-931b-4f199e3e1146
Preserve guarded root and launcher stat diagnostics while retaining the pinned, shell-free Playwright MCP launcher and deterministic npm-resolution seams. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Classify an exact-parent launcher resolution as escaping the declared plugin root. - Add deterministic coverage for the exact-parent containment boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e799af6-1da3-4859-931b-4f199e3e1146
Preserve the exact-parent containment boundary fix while retaining the pinned, shell-free Playwright MCP launcher and prior deterministic test seams. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Propagate the final #385 tip through the shell and URL hardening stack. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…iyanshu92/readme-safety-guidance
…iyanshu92/fix-power-pages-mcp-root
Inherit current main through the lower stack while preserving the exact Playwright MCP pin, shell-free launcher, npx error handling, and hardened root-resolution coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
There was a problem hiding this comment.
Review details
Suppressed comments (2)
plugins/power-pages/scripts/lib/ensure-pipelines-host-detect.js:100
- This error message is now very generic because getAuthToken returns null and the underlying az stderr is not available here. Consider adding actionable guidance so users know how to remediate (e.g., install Azure CLI / run
az login).
function getDataverseToken(originUrl, getTokenImpl) {
const trustedOrigin = helpers.validateDataverseEnvironmentUrl(originUrl);
if (typeof getTokenImpl === 'function') return getTokenImpl(trustedOrigin);
const token = helpers.getAuthToken(trustedOrigin);
if (!token) throw new Error(`az token acquisition failed for ${trustedOrigin}`);
return token;
plugins/power-pages/scripts/lib/list-tenant-envs.js:182
- The new failure path drops the underlying Azure CLI error details, which makes diagnosing auth issues difficult (e.g., az not installed vs not logged in vs network). Since getAuthToken returns null, consider including actionable guidance in this thrown message (at least suggest running
az login, and mention--allow-no-subscriptionsif relevant).
function getDataverseToken(originUrl, getTokenImpl) {
const trustedOrigin = helpers.validateDataverseEnvironmentUrl(originUrl);
if (typeof getTokenImpl === 'function') return getTokenImpl(trustedOrigin);
const token = helpers.getAuthToken(trustedOrigin);
if (!token) throw new Error(`az token acquisition failed for ${trustedOrigin}`);
return token;
- Files reviewed: 21/21 changed files
- Comments generated: 0 new
- Review effort level: Lite
Security impact
azandpacshell command strings in the affected Power Pages paths with argument arrays andshell: false.Tests
node --test plugins/power-pages/scripts/tests/(1,320 tests)