Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/poem-bot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/workflow-generator.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions actions/setup/js/collect_ndjson_output.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/// <reference types="@actions/github-script" />

const { getErrorMessage } = require("./error_helpers.cjs");
const { repairJson } = require("./json_repair_helpers.cjs");
const { repairJson, sanitizePrototypePollution } = require("./json_repair_helpers.cjs");
const { AGENT_OUTPUT_FILENAME, TMP_GH_AW_PATH } = require("./constants.cjs");

async function main() {
Expand Down Expand Up @@ -128,11 +128,15 @@ async function main() {
}
function parseJsonWithRepair(jsonStr) {
try {
return JSON.parse(jsonStr);
const parsed = JSON.parse(jsonStr);
// Sanitize the parsed object to prevent prototype pollution
return sanitizePrototypePollution(parsed);
} catch (originalError) {
try {
const repairedJson = repairJson(jsonStr);
return JSON.parse(repairedJson);
const parsed = JSON.parse(repairedJson);
// Sanitize the parsed object to prevent prototype pollution
return sanitizePrototypePollution(parsed);
} catch (repairError) {
core.info(`invalid input json: ${jsonStr}`);
const originalMsg = originalError instanceof Error ? originalError.message : String(originalError);
Expand Down
98 changes: 97 additions & 1 deletion actions/setup/js/json_repair_helpers.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,101 @@
// @ts-check

/**
* Sanitizes an object to remove dangerous prototype pollution keys using a stack-based algorithm.
* This function removes keys that could be used for prototype pollution attacks:
* - __proto__: JavaScript's prototype chain accessor
* - constructor: Object constructor property
* - prototype: Function prototype property
*
* Uses an iterative approach with a stack to handle deeply nested structures and
* protect against stack overflow from malicious recursive object trees.
*
* @param {any} obj - The object to sanitize (can be any type)
* @returns {any} The sanitized object with dangerous keys removed
*
* @example
* // Removes __proto__ key
* sanitizePrototypePollution({name: "test", __proto__: {isAdmin: true}})
* // Returns: {name: "test"}
*
* @example
* // Handles nested objects
* sanitizePrototypePollution({outer: {__proto__: {bad: true}, safe: "value"}})
* // Returns: {outer: {safe: "value"}}
*/
function sanitizePrototypePollution(obj) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use stack based algo instead of recursion, protect against recursive object tree

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converted to stack-based algorithm with WeakMap for circular reference tracking. This prevents stack overflow from deeply nested structures and handles circular references safely. Commit 693338c.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add more tests for this function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 9 new tests covering:

  • Circular references (simple, nested, with dangerous keys)
  • Very deep nesting (1000 levels)
  • Mixed circular and nested structures
  • Repeated object references
  • Array circular references
  • Malicious deeply nested attack with circularity

Total: 76 tests (up from 67). Commit 693338c.

// Handle non-objects (primitives, null, undefined)
if (obj === null || typeof obj !== "object") {
return obj;
}

// Dangerous keys that can be used for prototype pollution
const dangerousKeys = ["__proto__", "constructor", "prototype"];

// Track visited objects to handle circular references
const seen = new WeakMap();

// Stack-based traversal to avoid recursion and stack overflow
// Each entry: { source: original object, target: sanitized object, parent: parent target, key: property key }
const stack = [];
const root = Array.isArray(obj) ? [] : {};
seen.set(obj, root);
stack.push({ source: obj, target: root, parent: null, key: null });

while (stack.length > 0) {
const item = stack.pop();
if (!item) continue;
const { source, target } = item;

if (Array.isArray(source)) {
// Process array elements
for (let i = 0; i < source.length; i++) {
const value = source[i];
if (value === null || typeof value !== "object") {
// Primitive value - copy directly
target[i] = value;
} else if (seen.has(value)) {
// Circular reference detected - use existing sanitized object
target[i] = seen.get(value);
} else {
// New object or array - create sanitized version and add to stack
const newTarget = Array.isArray(value) ? [] : {};
target[i] = newTarget;
seen.set(value, newTarget);
stack.push({ source: value, target: newTarget, parent: target, key: i });
}
}
} else {
// Process object properties
for (const key in source) {
// Skip dangerous keys
if (dangerousKeys.includes(key)) {
continue;
}
// Only process own properties (not inherited)
if (Object.prototype.hasOwnProperty.call(source, key)) {
const value = source[key];
if (value === null || typeof value !== "object") {
// Primitive value - copy directly
target[key] = value;
} else if (seen.has(value)) {
// Circular reference detected - use existing sanitized object
target[key] = seen.get(value);
} else {
// New object or array - create sanitized version and add to stack
const newTarget = Array.isArray(value) ? [] : {};
target[key] = newTarget;
seen.set(value, newTarget);
stack.push({ source: value, target: newTarget, parent: target, key: key });
}
}
}
}
}

return root;
}

/**
* Attempts to repair malformed JSON strings using various heuristics.
* This function applies multiple repair strategies to fix common JSON formatting issues:
Expand Down Expand Up @@ -76,4 +172,4 @@ function repairJson(jsonStr) {
return repaired;
}

module.exports = { repairJson };
module.exports = { repairJson, sanitizePrototypePollution };
Loading
Loading