Skip to content

Commit 541ca31

Browse files
Priyanshu Agrawalclaude
authored andcommitted
Address PR review comments: security, robustness, and correctness fixes
- Fix frontmatter parsing in ensure-skill-version-check.js to use line-based regex instead of indexOf, handling CRLF and avoiding false matches - Reject newline characters in yamlStr() for both serverlogic and cloudflow metadata scripts to prevent invalid YAML generation - Accept optional catch binding syntax (catch { }) in validator - Strip comments and string literals before disallowed-token checks in validator to prevent false positives - Add HTTPS URL validation in list-custom-actions.js before passing to getAuthToken() to prevent shell injection - Fix header comment in list-cloud-flows.js to match actual output shape - Remove duplicate test in list-cloud-flows.test.js Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 261ef57 commit 541ca31

7 files changed

Lines changed: 80 additions & 20 deletions

File tree

plugins/power-pages/scripts/list-custom-actions.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ async function main() {
150150
}
151151

152152
const cleanUrl = envUrl.replace(/\/+$/, '');
153+
154+
// Validate the URL is a strict HTTPS URL to prevent shell injection via getAuthToken
155+
try {
156+
const parsed = new URL(cleanUrl);
157+
if (parsed.protocol !== 'https:') {
158+
process.stderr.write('Error: environmentUrl must use HTTPS.\n');
159+
process.exit(1);
160+
}
161+
} catch {
162+
process.stderr.write(`Error: Invalid URL: "${cleanUrl}"\n`);
163+
process.exit(1);
164+
}
165+
153166
const token = getAuthToken(cleanUrl);
154167
if (!token) {
155168
process.stderr.write(

plugins/power-pages/scripts/tests/list-cloud-flows.test.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,3 @@ test('fails when pac CLI is not available (empty PATH)', () => {
2727
assert.match(result.stderr, /pac auth who|environment|unable/i);
2828
});
2929

30-
test('fails when az CLI is not available for token (empty PATH)', () => {
31-
// With empty PATH, pac auth who also fails, so the first error fires
32-
const result = runListCloudFlows({ PATH: '' });
33-
assert.notEqual(result.status, 0);
34-
// Should fail at authentication stage
35-
assert.ok(result.stderr.length > 0, 'should produce an error message');
36-
});

plugins/power-pages/skills/add-cloud-flow/scripts/create-cloud-flow-metadata.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ if (fs.existsSync(filePath)) {
110110
const uuid = generateUuid();
111111

112112
// Serialize a string value safely for YAML: always single-quote, escaping internal single quotes.
113+
// Rejects newlines since single-quoted YAML scalars cannot span lines without breaking structure.
113114
function yamlStr(val) {
115+
if (/[\r\n]/.test(val)) {
116+
process.stderr.write(`Error: Value contains newline characters which are not supported in single-line YAML fields: "${val.slice(0, 50)}..."\n`);
117+
process.exit(1);
118+
}
114119
return "'" + val.replace(/'/g, "''") + "'";
115120
}
116121

plugins/power-pages/skills/add-cloud-flow/scripts/list-cloud-flows.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// node list-cloud-flows.js
1616
//
1717
// Output (JSON to stdout):
18-
// { "flows": [ { "id": "<guid>", "name": "<string>", "description": "<string>", "state": "Active|Draft" } ] }
18+
// { "flows": [ { "id": "<guid>", "flowRpName": "<string>", "displayName": "<string>", "description": "<string>", "state": "Active|Draft" } ] }
1919
//
2020
// Exits with code 1 on errors (messages to stderr).
2121

plugins/power-pages/skills/add-server-logic/scripts/create-serverlogic-metadata.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ if (fs.existsSync(filePath)) {
7777
const uuid = generateUuid();
7878

7979
// Serialize a string value safely for YAML: always single-quote, escaping internal single quotes.
80+
// Rejects newlines since single-quoted YAML scalars cannot span lines without breaking structure.
8081
function yamlStr(val) {
82+
if (/[\r\n]/.test(val)) {
83+
console.error(`Error: Value contains newline characters which are not supported in single-line YAML fields: "${val.slice(0, 50)}..."`);
84+
process.exit(1);
85+
}
8186
return "'" + val.replace(/'/g, "''") + "'";
8287
}
8388

plugins/power-pages/skills/add-server-logic/scripts/validate-serverlogic.js

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ runValidation((cwd) => {
134134
const fnBody = content.slice(bodyStart, bodyEnd);
135135
if (!/\btry\s*\{/.test(fnBody)) {
136136
errors.push(`${dirName}.js: function '${fn}' is missing try/catch error handling`);
137-
} else if (!/\bcatch\s*\(/.test(fnBody)) {
137+
} else if (!/\bcatch\s*[({]/.test(fnBody)) {
138138
errors.push(`${dirName}.js: function '${fn}' has try but is missing a catch block`);
139139
}
140140
if (isAsync && !/\bawait\b/.test(fnBody)) {
@@ -176,26 +176,30 @@ runValidation((cwd) => {
176176
}
177177
}
178178

179+
// Strip comments and string literals so disallowed-token checks don't false-positive
180+
// on occurrences inside documentation comments or string values.
181+
const strippedContent = stripCommentsAndStrings(content);
182+
179183
// Check: no require/import statements
180-
if (/\brequire\s*\(/.test(content) || /\bimport\s+/.test(content)) {
184+
if (/\brequire\s*\(/.test(strippedContent) || /\bimport\s+/.test(strippedContent)) {
181185
errors.push(`${dirName}.js: contains require() or import — no external dependencies allowed`);
182186
}
183187

184188
// Check: no browser APIs
185189
for (const api of BROWSER_APIS) {
186190
const regex = new RegExp(`\\b${api}`, 'g');
187-
if (regex.test(content)) {
191+
if (regex.test(strippedContent)) {
188192
errors.push(`${dirName}.js: contains browser API '${api.replace('\\.', '')}' — not available in server logic runtime`);
189193
}
190194
}
191195

192196
// Check: no console usage
193-
if (/\bconsole\s*\./.test(content)) {
197+
if (/\bconsole\s*\./.test(strippedContent)) {
194198
errors.push(`${dirName}.js: contains console.* — use Server.Logger instead`);
195199
}
196200

197201
// Check: no 'function delete()'
198-
if (/(?:async\s+)?function\s+delete\s*\(/m.test(content)) {
202+
if (/(?:async\s+)?function\s+delete\s*\(/m.test(strippedContent)) {
199203
errors.push(`${dirName}.js: uses 'function delete()' — 'delete' is a reserved word, use 'del' instead`);
200204
}
201205
}
@@ -221,6 +225,45 @@ function findServerLogicDirs(dir) {
221225
return dirs;
222226
}
223227

228+
/**
229+
* Replace all comments and string literals with whitespace so that regex
230+
* checks for disallowed tokens don't match inside non-code contexts.
231+
*/
232+
function stripCommentsAndStrings(src) {
233+
let result = '';
234+
let i = 0;
235+
while (i < src.length) {
236+
const ch = src[i];
237+
// Line comment
238+
if (ch === '/' && src[i + 1] === '/') {
239+
while (i < src.length && src[i] !== '\n') { result += ' '; i++; }
240+
continue;
241+
}
242+
// Block comment
243+
if (ch === '/' && src[i + 1] === '*') {
244+
result += ' '; i++;
245+
result += ' '; i++;
246+
while (i < src.length - 1 && !(src[i] === '*' && src[i + 1] === '/')) { result += ' '; i++; }
247+
if (i < src.length) { result += ' '; i++; }
248+
if (i < src.length) { result += ' '; i++; }
249+
continue;
250+
}
251+
// String literal
252+
if (ch === '\'' || ch === '"' || ch === '`') {
253+
result += ' '; i++;
254+
while (i < src.length && src[i] !== ch) {
255+
if (src[i] === '\\') { result += ' '; i++; }
256+
result += ' '; i++;
257+
}
258+
if (i < src.length) { result += ' '; i++; }
259+
continue;
260+
}
261+
result += ch;
262+
i++;
263+
}
264+
return result;
265+
}
266+
224267
/**
225268
* Find all top-level function names using brace-depth tracking.
226269
* Skips string literals and comments so nested functions are not reported.

scripts/ensure-skill-version-check.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,17 @@ function hasVersionCheck(content) {
3333
}
3434

3535
function addVersionCheck(content) {
36-
// Find the closing --- of the YAML frontmatter (second occurrence of ---)
37-
const firstIdx = content.indexOf('---');
38-
if (firstIdx === -1) return content;
39-
const secondIdx = content.indexOf('---', firstIdx + 3);
40-
if (secondIdx === -1) return content;
41-
const insertPos = secondIdx + 3;
36+
// Match YAML frontmatter: starts with --- on its own line, ends with --- on its own line.
37+
// Use line-based matching to avoid false positives from --- inside body or values,
38+
// and handle both LF and CRLF line endings.
39+
const match = content.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/);
40+
if (!match) return content;
41+
const insertPos = match[0].length;
4242
return (
4343
content.slice(0, insertPos) +
44-
'\n\n' +
44+
'\n' +
4545
VERSION_CHECK_LINE +
46+
'\n' +
4647
content.slice(insertPos)
4748
);
4849
}

0 commit comments

Comments
 (0)