From e7ff9ceced46c06a51937f6963471eec57a86313 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:38:43 +0000 Subject: [PATCH 1/5] Initial plan From 79fc2bbfaa3b1a565c3cb51aacec09f3847b060d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:44:45 +0000 Subject: [PATCH 2/5] Initial plan Co-authored-by: szabta89 <1330202+szabta89@users.noreply.github.com> --- .github/workflows/dependabot-campaign.lock.yml | 9 +++++++-- .github/workflows/dependabot-worker.lock.yml | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependabot-campaign.lock.yml b/.github/workflows/dependabot-campaign.lock.yml index 1bbd297c733..2f448c6266f 100644 --- a/.github/workflows/dependabot-campaign.lock.yml +++ b/.github/workflows/dependabot-campaign.lock.yml @@ -671,8 +671,13 @@ jobs: echo GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' >> "$GITHUB_ENV" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) diff --git a/.github/workflows/dependabot-worker.lock.yml b/.github/workflows/dependabot-worker.lock.yml index 7ff762bb9ba..8dd370c66de 100644 --- a/.github/workflows/dependabot-worker.lock.yml +++ b/.github/workflows/dependabot-worker.lock.yml @@ -780,8 +780,13 @@ jobs: echo GH_AW_MCP_CLI_SERVERS='["safeoutputs"]' >> "$GITHUB_ENV" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') - DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || echo '0') - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' + case "${DOCKER_HOST:-}" in + unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;; + /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; + * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; + esac + DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) From 5fc6402d19f6c141cf2b15454ed6d99079157f43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:46:02 +0000 Subject: [PATCH 3/5] fix: decode named invisible-char HTML entities in decodeHtmlEntities to prevent @mention bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend `decodeHtmlEntities` in `sanitize_content_core.cjs` to handle the named entity forms of invisible/formatting characters (­, ‌, ‍, ‎, ‏, ​, ⁠, ⁡/⁡, ⁢/⁢, ⁣/⁣) including double-encoded variants (&shy; etc.). Without this fix, `@­victim` (using the named entity form) survived `decodeHtmlEntities` and defeated `neutralizeAllMentions` because `&` is not matched by `[A-Za-z0-9]` in the mention regex. After GitHub renders the output, the entity decodes to an invisible char and the result appears as `@victim` to readers — a partial bypass of the fix from gh-aw#24154 / gh-aw-security#2086. Each named entity is now decoded to its actual Unicode code point so that hardenUnicodeText Step 3 strips it before mention neutralization. Regression tests added in sanitize_content.test.cjs. Co-authored-by: szabta89 <1330202+szabta89@users.noreply.github.com> --- actions/setup/js/sanitize_content.test.cjs | 75 ++++++++++++++++++++++ actions/setup/js/sanitize_content_core.cjs | 32 ++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/sanitize_content.test.cjs b/actions/setup/js/sanitize_content.test.cjs index f139f840f69..e6e11467917 100644 --- a/actions/setup/js/sanitize_content.test.cjs +++ b/actions/setup/js/sanitize_content.test.cjs @@ -2425,6 +2425,81 @@ describe("sanitize_content.cjs", () => { // Idempotency: a second pass on the decoded result should not re-introduce > expect(twice).toBe(once); }); + + describe("named invisible-character entities — @mention bypass prevention", () => { + // These tests cover the bypass described in gh-aw#24154 / gh-aw-security#2086. + // Named entity forms of invisible characters must be decoded before Step 3 + // strips the resulting code points, otherwise the "&" character prevents + // neutralizeAllMentions from matching the @ sign. + + it("should decode ­ (soft hyphen U+00AD) and neutralize @mention", () => { + expect(sanitizeContent("@­victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode &shy; (double-encoded soft hyphen) and neutralize @mention", () => { + expect(sanitizeContent("@&shy;victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ‌ (zero-width non-joiner U+200C) and neutralize @mention", () => { + expect(sanitizeContent("@‌victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ‍ (zero-width joiner U+200D) and neutralize @mention", () => { + expect(sanitizeContent("@‍victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ‎ (left-to-right mark U+200E) and neutralize @mention", () => { + expect(sanitizeContent("@‎victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ‏ (right-to-left mark U+200F) and neutralize @mention", () => { + expect(sanitizeContent("@‏victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ​ (U+200B) and neutralize @mention", () => { + expect(sanitizeContent("@​victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁠ (word joiner U+2060) and neutralize @mention", () => { + expect(sanitizeContent("@⁠victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁡ (invisible function application U+2061) and neutralize @mention", () => { + expect(sanitizeContent("@⁡victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁡ (U+2061) and neutralize @mention", () => { + expect(sanitizeContent("@⁡victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁢ (invisible times U+2062) and neutralize @mention", () => { + expect(sanitizeContent("@⁢victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁢ (U+2062) and neutralize @mention", () => { + expect(sanitizeContent("@⁢victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁣ (invisible separator U+2063) and neutralize @mention", () => { + expect(sanitizeContent("@⁣victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode ⁣ (U+2063) and neutralize @mention", () => { + expect(sanitizeContent("@⁣victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode multiple named invisible entities between @ and username", () => { + expect(sanitizeContent("@­‌‎victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode case-insensitive named invisible entities", () => { + expect(sanitizeContent("@&SHY;victim say hi")).toBe("`@victim` say hi"); + expect(sanitizeContent("@&ZWNJ;victim say hi")).toBe("`@victim` say hi"); + expect(sanitizeContent("@&ZWJ;victim say hi")).toBe("`@victim` say hi"); + expect(sanitizeContent("@&LRM;victim say hi")).toBe("`@victim` say hi"); + expect(sanitizeContent("@&RLM;victim say hi")).toBe("`@victim` say hi"); + }); + }); }); describe("template delimiter neutralization (T24)", () => { diff --git a/actions/setup/js/sanitize_content_core.cjs b/actions/setup/js/sanitize_content_core.cjs index dc2bacdd199..b41b233aabf 100644 --- a/actions/setup/js/sanitize_content_core.cjs +++ b/actions/setup/js/sanitize_content_core.cjs @@ -966,8 +966,10 @@ function applyTruncation(content, maxLength) { /** * Decodes HTML entities to prevent bypass of @mention detection and to ensure * HTML-encoded characters do not persist in sanitized output (e.g. > in titles). - * Handles named entities (e.g., @, >, <, &), decimal entities (e.g., @), - * and hex entities (e.g., @), including double-encoded variants (e.g., &commat;). + * Handles named entities (e.g., @, >, <, &, ­, ‌, ‍, + * ‎, ‏, ​, ⁠, ⁡/⁡, ⁢/⁢, + * ⁣/⁣), decimal entities (e.g., @), and hex entities (e.g., @), + * including double-encoded variants (e.g., &commat;). * * @param {string} text - Input text that may contain HTML entities * @returns {string} Text with HTML entities decoded @@ -993,6 +995,32 @@ function decodeHtmlEntities(text) { // & and &amp; → & (decoded after gt/lt so &gt; is already handled above) result = result.replace(/&(?:amp;)?amp;/gi, "&"); + // Decode named entities for invisible/formatting characters that are stripped in + // hardenUnicodeText Step 3. Without this, the named-entity forms survive entity + // decoding and defeat neutralizeAllMentions (e.g. @­user passes the mention + // regex because "&" is not in [A-Za-z0-9], then renders as @user in GitHub). + // Each entity is decoded to its actual Unicode code point so Step 3 can strip it. + // ­ and &shy; → U+00AD (soft hyphen) + result = result.replace(/&(?:amp;)?shy;/gi, "\u00AD"); + // ‌ and &zwnj; → U+200C (zero-width non-joiner) + result = result.replace(/&(?:amp;)?zwnj;/gi, "\u200C"); + // ‍ and &zwj; → U+200D (zero-width joiner) + result = result.replace(/&(?:amp;)?zwj;/gi, "\u200D"); + // ‎ and &lrm; → U+200E (left-to-right mark) + result = result.replace(/&(?:amp;)?lrm;/gi, "\u200E"); + // ‏ and &rlm; → U+200F (right-to-left mark) + result = result.replace(/&(?:amp;)?rlm;/gi, "\u200F"); + // ​ and &ZeroWidthSpace; → U+200B (zero-width space) + result = result.replace(/&(?:amp;)?ZeroWidthSpace;/gi, "\u200B"); + // ⁠ and &NoBreak; → U+2060 (word joiner) + result = result.replace(/&(?:amp;)?NoBreak;/gi, "\u2060"); + // ⁡ / ⁡ and double-encoded variants → U+2061 (invisible function application) + result = result.replace(/&(?:amp;)?(?:af|ApplyFunction);/gi, "\u2061"); + // ⁢ / ⁢ and double-encoded variants → U+2062 (invisible times) + result = result.replace(/&(?:amp;)?(?:it|InvisibleTimes);/gi, "\u2062"); + // ⁣ / ⁣ and double-encoded variants → U+2063 (invisible separator) + result = result.replace(/&(?:amp;)?(?:ic|InvisibleComma);/gi, "\u2063"); + // Decode decimal entities (including double-encoded variants) // @ and &#64; → @ // &#NNN; and &#NNN; → corresponding character From dbc1ed6436ae1c63b7ca6b91ebc6c3346a19f111 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:49:39 +0000 Subject: [PATCH 4/5] fix comment: use ­ entity notation instead of literal U+00AD in comment Co-authored-by: szabta89 <1330202+szabta89@users.noreply.github.com> --- actions/setup/js/sanitize_content_core.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/sanitize_content_core.cjs b/actions/setup/js/sanitize_content_core.cjs index b41b233aabf..3291054350f 100644 --- a/actions/setup/js/sanitize_content_core.cjs +++ b/actions/setup/js/sanitize_content_core.cjs @@ -997,7 +997,7 @@ function decodeHtmlEntities(text) { // Decode named entities for invisible/formatting characters that are stripped in // hardenUnicodeText Step 3. Without this, the named-entity forms survive entity - // decoding and defeat neutralizeAllMentions (e.g. @­user passes the mention + // decoding and defeat neutralizeAllMentions (e.g. @­user passes the mention // regex because "&" is not in [A-Za-z0-9], then renders as @user in GitHub). // Each entity is decoded to its actual Unicode code point so Step 3 can strip it. // ­ and &shy; → U+00AD (soft hyphen) From 18f331dfb262106c2d2f87563d3e7c641c52ab97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 15:44:39 +0000 Subject: [PATCH 5/5] fix: add &ip;/&InvisiblePlus; (U+2064) entity decoding and socket validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add &ip;/&InvisiblePlus; named entity → U+2064 (INVISIBLE PLUS) to decodeHtmlEntities, closing the gap identified by the reviewer: Step 3's strip regex covers \u2060-\u2064 but the named entity block only reached U+2063. Add two matching regression tests. - Add test -S socket validation in dependabot-campaign.lock.yml and dependabot-worker.lock.yml: if DOCKER_SOCK_PATH derived from DOCKER_HOST is not a socket, fall back to /var/run/docker.sock before stat/mount. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dependabot-campaign.lock.yml | 3 +++ .github/workflows/dependabot-worker.lock.yml | 3 +++ actions/setup/js/sanitize_content.test.cjs | 8 ++++++++ actions/setup/js/sanitize_content_core.cjs | 3 +++ 4 files changed, 17 insertions(+) diff --git a/.github/workflows/dependabot-campaign.lock.yml b/.github/workflows/dependabot-campaign.lock.yml index 2f448c6266f..a109c14673b 100644 --- a/.github/workflows/dependabot-campaign.lock.yml +++ b/.github/workflows/dependabot-campaign.lock.yml @@ -676,6 +676,9 @@ jobs: /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac + if ! test -S "$DOCKER_SOCK_PATH"; then + DOCKER_SOCK_PATH=/var/run/docker.sock + fi DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' diff --git a/.github/workflows/dependabot-worker.lock.yml b/.github/workflows/dependabot-worker.lock.yml index 8dd370c66de..f18e2598b61 100644 --- a/.github/workflows/dependabot-worker.lock.yml +++ b/.github/workflows/dependabot-worker.lock.yml @@ -785,6 +785,9 @@ jobs: /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;; * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;; esac + if ! test -S "$DOCKER_SOCK_PATH"; then + DOCKER_SOCK_PATH=/var/run/docker.sock + fi DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0') export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' diff --git a/actions/setup/js/sanitize_content.test.cjs b/actions/setup/js/sanitize_content.test.cjs index e6e11467917..5e3bc4d7636 100644 --- a/actions/setup/js/sanitize_content.test.cjs +++ b/actions/setup/js/sanitize_content.test.cjs @@ -2488,6 +2488,14 @@ describe("sanitize_content.cjs", () => { expect(sanitizeContent("@⁣victim say hi")).toBe("`@victim` say hi"); }); + it("should decode &ip; (invisible plus U+2064) and neutralize @mention", () => { + expect(sanitizeContent("@&ip;victim say hi")).toBe("`@victim` say hi"); + }); + + it("should decode &InvisiblePlus; (U+2064) and neutralize @mention", () => { + expect(sanitizeContent("@&InvisiblePlus;victim say hi")).toBe("`@victim` say hi"); + }); + it("should decode multiple named invisible entities between @ and username", () => { expect(sanitizeContent("@­‌‎victim say hi")).toBe("`@victim` say hi"); }); diff --git a/actions/setup/js/sanitize_content_core.cjs b/actions/setup/js/sanitize_content_core.cjs index 3291054350f..e66256bcdc4 100644 --- a/actions/setup/js/sanitize_content_core.cjs +++ b/actions/setup/js/sanitize_content_core.cjs @@ -1020,6 +1020,9 @@ function decodeHtmlEntities(text) { result = result.replace(/&(?:amp;)?(?:it|InvisibleTimes);/gi, "\u2062"); // ⁣ / ⁣ and double-encoded variants → U+2063 (invisible separator) result = result.replace(/&(?:amp;)?(?:ic|InvisibleComma);/gi, "\u2063"); + // &ip; / &InvisiblePlus; and double-encoded variants → U+2064 (invisible plus) + // Note: U+2064 is the upper bound of the \u2060-\u2064 range stripped in Step 3. + result = result.replace(/&(?:amp;)?(?:ip|InvisiblePlus);/gi, "\u2064"); // Decode decimal entities (including double-encoded variants) // @ and &#64; → @