From 559681ad950ab855991de52f02be46c91461bad9 Mon Sep 17 00:00:00 2001 From: Assmira Lahcen Date: Wed, 19 Aug 2026 19:34:04 +0000 Subject: [PATCH] fix(security): redact URL credentials, harden redirect/body handling, bump @actions/github - redact user:password userinfo from console, JSON, and Markdown reports - render untrusted URLs, file paths, and error text as unescapable inline code so scanned docs cannot inject Markdown/HTML into PR comments - reject redirects to non-http(s) protocols and cap response bodies at 5MB - mask the action github-token via core.setSecret - upgrade @actions/github to ^9.1.1 to drop vulnerable undici 5.x --- package-lock.json | 311 ++++++++++++++++++++----------------------- package.json | 2 +- src/checker.ts | 7 + src/github.ts | 1 + src/report.ts | 32 ++++- src/utils.ts | 20 ++- test/checker.test.ts | 48 ++++++- test/report.test.ts | 29 ++++ test/utils.test.ts | 15 +++ 9 files changed, 287 insertions(+), 178 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61354f6..0689e22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@actions/core": "^2.0.0", - "@actions/github": "^7.0.0", + "@actions/github": "^9.1.1", "@actions/io": "^1.1.3", "axios": "^1.7.7", "chalk": "^5.3.0", @@ -58,18 +58,18 @@ } }, "node_modules/@actions/github": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-7.0.0.tgz", - "integrity": "sha512-PyGODO938aoBTZd/IfN/+e+Pd5hUcVpyf+thm4CPESLeqhdSkq5QwMTGX9v84XHE1ifmHWBQ60KB8kIgm96opw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-9.1.1.tgz", + "integrity": "sha512-tL5JbYOBZHc0ngEnCsaDcryUizIUIlQyIMwy1Wkx93H5HzbBJ7TbiPx2PnFjBwZW0Vh05JmfFZhecE6gglYegA==", "license": "MIT", "dependencies": { - "@actions/http-client": "^3.0.1", - "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.2.2", - "@octokit/plugin-rest-endpoint-methods": "^10.4.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "undici": "^5.28.5" + "@actions/http-client": "^3.0.2", + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0", + "@octokit/request": "^10.0.7", + "@octokit/request-error": "^7.1.0", + "undici": "^6.23.0" } }, "node_modules/@actions/http-client": { @@ -82,15 +82,6 @@ "undici": "^6.23.0" } }, - "node_modules/@actions/http-client/node_modules/undici": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", - "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/@actions/io": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", @@ -754,15 +745,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -888,161 +870,161 @@ } }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", - "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.7.tgz", + "integrity": "sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==", "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.1.0", - "@octokit/request": "^8.4.1", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.4", + "@octokit/request": "^10.0.13", + "@octokit/request-error": "^7.1.1", + "@octokit/types": "^17.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.4.tgz", + "integrity": "sha512-f1cOWoHPmxryJFknxbtDdjODWfV8A9tc8Aae6ermXPNgHFZ/x91AtHIz4gicEjL8hkJiip+u21QHJORfBv/qiA==", "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^17.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/graphql": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", - "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.4.tgz", + "integrity": "sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==", "license": "MIT", "dependencies": { - "@octokit/request": "^8.4.1", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^10.0.13", + "@octokit/types": "^17.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz", + "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==", "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", - "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", "license": "MIT", "dependencies": { - "@octokit/types": "^12.6.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": "5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", - "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", "license": "MIT", "dependencies": { - "@octokit/types": "^12.6.0" + "@octokit/types": "^16.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" }, "peerDependencies": { - "@octokit/core": "5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", - "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", "license": "MIT" }, "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", - "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^20.0.0" + "@octokit/openapi-types": "^27.0.0" } }, "node_modules/@octokit/request": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", - "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.14.tgz", + "integrity": "sha512-bgWgiSfFS689/AxQDarU+b3Qu1FYewfVr5vI/jhV84s7GQ3C2OsdRxukGGYKB37MCgwcS50UrfBLlHzhiG13sw==", "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.6", - "@octokit/request-error": "^5.1.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.1.1", + "@octokit/types": "^17.0.0", + "content-type": "^2.0.0", + "json-with-bigint": "^3.5.12", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/request-error": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", - "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.1.tgz", + "integrity": "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==", "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^17.0.0" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz", + "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@octokit/openapi-types": "^28.0.0" } }, "node_modules/@pkgjs/parseargs": { @@ -1629,16 +1611,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -2002,15 +1984,15 @@ "license": "MIT" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2214,6 +2196,19 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2271,12 +2266,6 @@ "node": ">=0.4.0" } }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "license": "ISC" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2889,15 +2878,15 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -3189,9 +3178,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -3232,6 +3221,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-with-bigint": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", + "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", + "license": "MIT" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3487,9 +3482,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -3522,15 +3517,6 @@ "node": ">=0.10.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -3733,9 +3719,9 @@ } }, "node_modules/postcss": { - "version": "8.5.22", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", - "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -3753,7 +3739,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4238,16 +4224,16 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/test-exclude/node_modules/emoji-regex": { @@ -4287,9 +4273,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -4631,15 +4617,12 @@ "license": "MIT" }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/undici-types": { @@ -4650,9 +4633,9 @@ "license": "MIT" }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "license": "ISC" }, "node_modules/uri-js": { @@ -5420,12 +5403,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 804cee9..009cddd 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@actions/core": "^2.0.0", - "@actions/github": "^7.0.0", + "@actions/github": "^9.1.1", "@actions/io": "^1.1.3", "axios": "^1.7.7", "chalk": "^5.3.0", diff --git a/src/checker.ts b/src/checker.ts index b3cbea0..5cd12c6 100644 --- a/src/checker.ts +++ b/src/checker.ts @@ -21,6 +21,8 @@ export interface CheckResult { } const MAX_REDIRECTS = 10; +/** Upper bound on a response body pulled into memory during the GET fallback. */ +const MAX_RESPONSE_BYTES = 5 * 1024 * 1024; /** * Checks a single URL and classifies the result. HTTP is always attempted @@ -126,6 +128,8 @@ export async function performRequest(url: string, timeout: number): Promise true, headers: { 'User-Agent': 'urldn-link-check (+https://github.com/urldn/link-check)', @@ -148,6 +152,9 @@ export async function performRequest(url: string, timeout: number): Promise= 300 && response.status < 400 && response.headers.location) { const nextUrl = new URL(String(response.headers.location), currentUrl).toString(); + if (!isValidUrl(nextUrl)) { + throw new Error('Redirect target uses an unsupported protocol'); + } redirectChain.push(currentUrl); currentUrl = nextUrl; continue; diff --git a/src/github.ts b/src/github.ts index 7e0b6a4..d6f6843 100644 --- a/src/github.ts +++ b/src/github.ts @@ -30,6 +30,7 @@ export async function runAction(): Promise { const output = core.getInput('output') || undefined; const commentOnPr = getBooleanInput('comment-on-pr', true); const token = core.getInput('github-token') || process.env.GITHUB_TOKEN; + if (token) core.setSecret(token); const config = resolveConfig({ path, diff --git a/src/report.ts b/src/report.ts index cb04f23..8bc7c8e 100644 --- a/src/report.ts +++ b/src/report.ts @@ -1,5 +1,6 @@ import { writeFile } from 'node:fs/promises'; import type { ScanReport, ScannedLinkResult } from './scanner.js'; +import { redactUrlCredentials } from './utils.js'; const URLDN_SHORTENER_URL = 'https://www.urldn.com'; @@ -29,11 +30,11 @@ export function toJsonReport(report: ScanReport): JsonReport { summary: report.summary, files: report.files, links: report.results.map((result) => ({ - url: result.url, + url: redactUrlCredentials(result.url), status: result.status, statusCode: result.statusCode, - redirectChain: result.redirectChain, - finalUrl: result.finalUrl, + redirectChain: result.redirectChain.map(redactUrlCredentials), + finalUrl: result.finalUrl ? redactUrlCredentials(result.finalUrl) : undefined, error: result.error, isHttp: result.isHttp, isDuplicate: result.isDuplicate, @@ -89,14 +90,17 @@ export function toMarkdownReport(report: ScanReport): string { lines.push(''); for (const result of problems) { - lines.push(`#### ${statusEmoji(result)} \`${result.url}\``); + lines.push(`#### ${statusEmoji(result)} ${inlineCode(redactUrlCredentials(result.url))}`); for (const occurrence of result.occurrences) { - lines.push(`- ${occurrence.file}:${occurrence.line}:${occurrence.column}`); + lines.push(`- ${inlineCode(`${occurrence.file}:${occurrence.line}:${occurrence.column}`)}`); } - if (result.error) lines.push(`- Error: ${result.error}`); + if (result.error) lines.push(`- Error: ${inlineCode(result.error)}`); if (result.statusCode) lines.push(`- HTTP status: ${result.statusCode}`); if (result.redirectChain.length > 0) { - lines.push(`- Redirect chain: ${[...result.redirectChain, result.finalUrl].join(' → ')}`); + const chain = [...result.redirectChain, result.finalUrl ?? ''] + .filter((hop) => hop.length > 0) + .map((hop) => inlineCode(redactUrlCredentials(hop))); + lines.push(`- Redirect chain: ${chain.join(' → ')}`); } if (result.isHttp) { lines.push('- ⚠️ Uses insecure `http://` — consider migrating to `https://`.'); @@ -116,6 +120,20 @@ export function toMarkdownReport(report: ScanReport): string { return lines.join('\n'); } +/** + * Renders arbitrary text as a Markdown inline code span that cannot be + * escaped. Content comes from scanned documents (URLs, file paths, server + * error messages), so it is untrusted: newlines are collapsed and the + * fence is grown to be longer than any backtick run inside the value. + */ +function inlineCode(value: string): string { + const text = value.replace(/[\r\n]+/g, ' '); + const longestRun = Math.max(0, ...Array.from(text.matchAll(/`+/g), (m) => m[0].length)); + const fence = '`'.repeat(longestRun + 1); + const padded = text.startsWith('`') || text.endsWith('`') ? ` ${text} ` : text; + return `${fence}${padded}${fence}`; +} + function statusEmoji(result: ScannedLinkResult): string { switch (result.status) { case 'ok': diff --git a/src/utils.ts b/src/utils.ts index 7f095ed..e952dc5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,6 +45,23 @@ export function isInsecureHttp(candidate: string): boolean { } } +/** + * Removes any `user:password@` userinfo from a URL so that credentials + * embedded in a link are never echoed into logs, reports, or PR comments. + * Non-URL strings are returned unchanged. + */ +export function redactUrlCredentials(candidate: string): string { + try { + const parsed = new URL(candidate); + if (!parsed.username && !parsed.password) return candidate; + if (parsed.username) parsed.username = '***'; + if (parsed.password) parsed.password = '***'; + return parsed.toString(); + } catch { + return candidate; + } +} + /** * Very small glob-ish matcher for ignore patterns. Supports `*` (any run of * non-slash characters) and `**` (any run of characters, including `/`). @@ -80,7 +97,8 @@ export function formatDuration(ms: number): string { } /** Truncates a URL for display purposes, keeping the start and end visible. */ -export function truncateUrl(url: string, maxLength = 70): string { +export function truncateUrl(rawUrl: string, maxLength = 70): string { + const url = redactUrlCredentials(rawUrl); if (url.length <= maxLength) return url; const headLength = Math.ceil((maxLength - 3) * 0.6); const tailLength = maxLength - 3 - headLength; diff --git a/test/checker.test.ts b/test/checker.test.ts index d64c437..f4fb6a7 100644 --- a/test/checker.test.ts +++ b/test/checker.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, it } from 'vitest'; -import { checkUrl } from '../src/checker.js'; +import { createServer, type Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { afterEach, describe, expect, it } from 'vitest'; +import { checkUrl, performRequest } from '../src/checker.js'; describe('checkUrl', () => { it('classifies a 200 response as ok', async () => { @@ -92,3 +94,45 @@ describe('checkUrl', () => { expect(result.error).toContain('ENOTFOUND'); }); }); + +describe('performRequest', () => { + let server: Server | undefined; + + afterEach(async () => { + if (!server) return; + await new Promise((resolve) => server?.close(() => resolve())); + server = undefined; + }); + + async function listen(handler: Parameters[1]): Promise { + server = createServer(handler); + await new Promise((resolve) => server?.listen(0, '127.0.0.1', () => resolve())); + const { port } = server.address() as AddressInfo; + return `http://127.0.0.1:${port}`; + } + + it('refuses to follow a redirect to a non-http(s) protocol', async () => { + const base = await listen((_req, res) => { + res.writeHead(302, { location: 'file:///etc/passwd' }); + res.end(); + }); + + await expect(performRequest(`${base}/redirect`, 5000)).rejects.toThrow( + /unsupported protocol/i, + ); + }); + + it('rejects responses whose body exceeds the size limit', async () => { + const base = await listen((req, res) => { + if (req.method === 'HEAD') { + res.writeHead(405); + res.end(); + return; + } + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end('x'.repeat(6 * 1024 * 1024)); + }); + + await expect(performRequest(`${base}/huge`, 10_000)).rejects.toThrow(/maxContentLength|size/i); + }); +}); diff --git a/test/report.test.ts b/test/report.test.ts index 6bf3809..ade7f51 100644 --- a/test/report.test.ts +++ b/test/report.test.ts @@ -78,6 +78,35 @@ describe('toMarkdownReport', () => { expect(markdown).not.toContain('No issues found'); }); + it('redacts credentials embedded in scanned URLs', () => { + const credentialResult: ScannedLinkResult = { + ...brokenResult, + url: 'https://user:hunter2@example.com/private', + redirectChain: ['https://user:hunter2@example.com/private'], + finalUrl: 'https://user:hunter2@example.com/moved', + }; + const report = buildReport([credentialResult]); + + const markdown = toMarkdownReport(report); + expect(markdown).not.toContain('hunter2'); + expect(markdown).toContain('https://***:***@example.com/private'); + + const json = toJsonReport(report); + expect(JSON.stringify(json)).not.toContain('hunter2'); + }); + + it('neutralizes Markdown injected through a URL or error message', () => { + const injectedResult: ScannedLinkResult = { + ...brokenResult, + url: 'https://example.com/`', + error: 'boom`\n\n### injected heading', + }; + const markdown = toMarkdownReport(buildReport([injectedResult])); + + expect(markdown).toContain('``https://example.com/```'); + expect(markdown).not.toContain('\n### injected heading'); + }); + it('suggests URLDN for long URLs', () => { const longResult: ScannedLinkResult = { ...okResult, diff --git a/test/utils.test.ts b/test/utils.test.ts index 3716780..db2afdd 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -5,10 +5,25 @@ import { isInsecureHttp, isValidUrl, matchesIgnorePattern, + redactUrlCredentials, runWithConcurrency, truncateUrl, } from '../src/utils.js'; +describe('redactUrlCredentials', () => { + it('replaces basic-auth userinfo with a placeholder', () => { + expect(redactUrlCredentials('https://alice:s3cret@example.com/docs')).toBe( + 'https://***:***@example.com/docs', + ); + expect(redactUrlCredentials('https://token@example.com/')).toBe('https://***@example.com/'); + }); + + it('leaves URLs without credentials and non-URLs untouched', () => { + expect(redactUrlCredentials('https://example.com/docs')).toBe('https://example.com/docs'); + expect(redactUrlCredentials('not a url')).toBe('not a url'); + }); +}); + describe('isValidUrl', () => { it('accepts http and https URLs', () => { expect(isValidUrl('https://example.com')).toBe(true);