From eb1758bebb886ffc94acf03e7a273d2da86a7fb7 Mon Sep 17 00:00:00 2001 From: Rob Anderson Date: Thu, 10 Sep 2026 11:28:45 -0700 Subject: [PATCH 1/4] quote variables, use actions SHAs --- .github/workflows/approve-or-deny-request.yml | 16 ++++++++-------- .github/workflows/initialize-request.yml | 18 +++++++++--------- .github/workflows/pr.yml | 6 +++--- .github/workflows/test.yml | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/approve-or-deny-request.yml b/.github/workflows/approve-or-deny-request.yml index b1d78cb..72477c7 100644 --- a/.github/workflows/approve-or-deny-request.yml +++ b/.github/workflows/approve-or-deny-request.yml @@ -15,7 +15,7 @@ jobs: payload: ${{ steps.issue_body_parser_request.outputs.payload }} steps: - name: Get JSON Data out of Issue Request - uses: peter-murray/issue-body-parser-action@v3 + uses: peter-murray/issue-body-parser-action@6f0770bae810c272cbdccf6f817b0759a9d82647 # v3 id: issue_body_parser_request with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -34,18 +34,18 @@ jobs: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} REQUEST_VERSION: ${{ fromJson(needs.parse-issue.outputs.payload).version }} run: | - if [ $REQUEST_VERSION == 'latest' ]; then + if [ "$REQUEST_VERSION" = 'latest' ]; then echo "Finding latest release of $OWNER/$REPO..." - export VERSION=`curl https://api.github.com/repos/$OWNER/$REPO/releases/latest | jq -r .name` + VERSION=$(curl "https://api.github.com/repos/$OWNER/$REPO/releases/latest" | jq -r .name) else - export VERSION=$REQUEST_VERSION + VERSION="$REQUEST_VERSION" fi echo "VERSION: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Check out scripts - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' check-latest: true @@ -54,7 +54,7 @@ jobs: cd .github/scripts npm install - name: Approve or deny request - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: VERSION: ${{ steps.get_version.outputs.version }} REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} diff --git a/.github/workflows/initialize-request.yml b/.github/workflows/initialize-request.yml index b8e648b..46b31db 100644 --- a/.github/workflows/initialize-request.yml +++ b/.github/workflows/initialize-request.yml @@ -15,7 +15,7 @@ jobs: payload: ${{ steps.issue_body_parser_request.outputs.payload }} steps: - name: Get JSON Data out of Issue Request - uses: peter-murray/issue-body-parser-action@v3 + uses: peter-murray/issue-body-parser-action@6f0770bae810c272cbdccf6f817b0759a9d82647 # v3 id: issue_body_parser_request with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -34,22 +34,22 @@ jobs: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} REQUEST_VERSION: ${{ fromJson(needs.parse-issue.outputs.payload).version }} run: | - if [ $REQUEST_VERSION == 'latest' ]; then + if [ "$REQUEST_VERSION" = 'latest' ]; then echo "Finding latest release of $OWNER/$REPO..." - export VERSION=`curl https://api.github.com/repos/$OWNER/$REPO/releases/latest | jq -r .name` + VERSION=$(curl "https://api.github.com/repos/$OWNER/$REPO/releases/latest" | jq -r .name) if [ "$VERSION" == "null" ]; then echo "No latest version found for $OWNER/$REPO" exit 1 fi else - export VERSION=$REQUEST_VERSION + VERSION="$REQUEST_VERSION" fi echo "VERSION: $VERSION" - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Check out scripts - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup Node - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' check-latest: true @@ -58,7 +58,7 @@ jobs: cd .github/scripts npm install - name: Create the repo ${{ fromJson(needs.parse-issue.outputs.payload).repo }}_${{ steps.get_version.outputs.version }} on GitHub Enterprise Server - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: VERSION: ${{ steps.get_version.outputs.version }} OWNER: ${{ fromJson(needs.parse-issue.outputs.payload).owner }} @@ -81,7 +81,7 @@ jobs: OWNER: ${{ fromJson(needs.parse-issue.outputs.payload).owner }} REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} run: | - git clone https://github.com/$OWNER/$REPO requested-action + git clone "https://github.com/$OWNER/$REPO" requested-action - name: Push requested action to private repo in $ACTIONS_APPROVED_ORG org on GitHub Enterprise Server env: REPO: ${{ fromJson(needs.parse-issue.outputs.payload).repo }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a8bd6f8..ff91da5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,8 +17,8 @@ jobs: working-directory: ./.github/scripts steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' check-latest: true @@ -28,7 +28,7 @@ jobs: working-directory: ${{ env.working-directory }} - run: npm test working-directory: ${{ env.working-directory }} - - uses: tintef/nyc-reporter-action@0.2.5 + - uses: tintef/nyc-reporter-action@66a40cea2d02fdb0ac8fa045a7355cbbf9b991a1 # 0.2.5 with: GITHUB_TOKEN: ${{ github.token }} SKIP_COVERAGE_FOLDER: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0e0d3e..4aad96b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,8 +12,8 @@ jobs: env: working-directory: ./.github/scripts steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 20 cache: npm From 39900dc613381742b36b14eace279a9cfdef449e Mon Sep 17 00:00:00 2001 From: Rob Anderson Date: Thu, 10 Sep 2026 11:40:36 -0700 Subject: [PATCH 2/4] validate owner and repo against github naming standards --- .github/scripts/test.mjs | 54 +++++++++++++++++++ .github/scripts/validate-request.mjs | 33 ++++++++++++ .github/workflows/approve-or-deny-request.yml | 12 +++++ .github/workflows/initialize-request.yml | 12 +++++ 4 files changed, 111 insertions(+) create mode 100644 .github/scripts/validate-request.mjs diff --git a/.github/scripts/test.mjs b/.github/scripts/test.mjs index 50385b4..5a1cc09 100644 --- a/.github/scripts/test.mjs +++ b/.github/scripts/test.mjs @@ -4,6 +4,7 @@ import fs from 'fs'; const { default: initialize } = await import('./initialize-request.mjs'); const { default: complete } = await import('./approve-or-deny-request.mjs'); +const { default: validateRequest } = await import('./validate-request.mjs'); import nock from 'nock'; nock.disableNetConnect(); @@ -65,6 +66,59 @@ test.after.each(() => { // nothing to do here }); +test("Validate GitHub owner and repository names", function () { + const validRequests = [ + { owner: "a", repo: "a" }, + { owner: "a-b", repo: ".github" }, + { owner: "a".repeat(39), repo: "a".repeat(100) }, + { owner: "GitHub", repo: "repo_name-1.0" } + ]; + + for (const request of validRequests) { + assert.equal(validateRequest(JSON.stringify(request)), request); + } +}); + +test("Reject invalid GitHub owner names", function () { + const invalidOwners = [ + "", + "-owner", + "owner-", + "owner--name", + "owner_name", + "owner/name", + "owner name", + "a".repeat(40) + ]; + + for (const owner of invalidOwners) { + assert.throws(() => validateRequest({ owner, repo: "repo" }), /Owner must/); + } +}); + +test("Reject invalid GitHub repository names", function () { + const invalidRepositories = [ + "", + "repo/name", + "repo name", + "repo$name", + "répo", + "a".repeat(101) + ]; + + for (const repo of invalidRepositories) { + assert.throws(() => validateRequest({ owner: "owner", repo }), /Repository must/); + } +}); + +test("Reject malformed request payloads", function () { + assert.throws(() => validateRequest("not json"), /valid JSON/); + assert.throws(() => validateRequest(null), /JSON object/); + assert.throws(() => validateRequest([]), /JSON object/); + assert.throws(() => validateRequest({ owner: "owner" }), /Repository must/); + assert.throws(() => validateRequest({ repo: "repo" }), /Owner must/); +}); + // Fail the workflow because the repo already exists test("Fail the workflow because the repo already exists", async function () { let mock = nock("https://github.robandpdx.demo-stack.com/api/v3"); diff --git a/.github/scripts/validate-request.mjs b/.github/scripts/validate-request.mjs new file mode 100644 index 0000000..db9a238 --- /dev/null +++ b/.github/scripts/validate-request.mjs @@ -0,0 +1,33 @@ +const OWNER_PATTERN = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/; +const REPOSITORY_PATTERN = /^[A-Za-z0-9._-]+$/; + +export default function validateRequest(payload) { + let request; + + try { + request = typeof payload === 'string' ? JSON.parse(payload) : payload; + } catch { + throw new Error('Request payload must be valid JSON'); + } + + if (!request || typeof request !== 'object' || Array.isArray(request)) { + throw new Error('Request payload must be a JSON object'); + } + + validateOwner(request.owner); + validateRepository(request.repo); + + return request; +} + +function validateOwner(owner) { + if (typeof owner !== 'string' || owner.length < 1 || owner.length > 39 || !OWNER_PATTERN.test(owner)) { + throw new Error('Owner must be 1-39 ASCII letters, digits, or single hyphens, and cannot begin or end with a hyphen'); + } +} + +function validateRepository(repo) { + if (typeof repo !== 'string' || repo.length < 1 || repo.length > 100 || !REPOSITORY_PATTERN.test(repo)) { + throw new Error('Repository must be 1-100 ASCII letters, digits, periods, hyphens, or underscores'); + } +} \ No newline at end of file diff --git a/.github/workflows/approve-or-deny-request.yml b/.github/workflows/approve-or-deny-request.yml index 72477c7..c9ed806 100644 --- a/.github/workflows/approve-or-deny-request.yml +++ b/.github/workflows/approve-or-deny-request.yml @@ -22,6 +22,18 @@ jobs: issue_id: ${{ github.event.issue.number }} payload_marker: request fail_on_missing: false + - name: Check out request validator + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate requested repository + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + REQUEST_PAYLOAD: ${{ steps.issue_body_parser_request.outputs.payload }} + with: + script: | + const { default: validateRequest } = await import('${{ github.workspace }}/.github/scripts/validate-request.mjs'); + validateRequest(process.env.REQUEST_PAYLOAD); approve-or-deny-request: runs-on: self-hosted needs: parse-issue diff --git a/.github/workflows/initialize-request.yml b/.github/workflows/initialize-request.yml index 46b31db..65f2748 100644 --- a/.github/workflows/initialize-request.yml +++ b/.github/workflows/initialize-request.yml @@ -22,6 +22,18 @@ jobs: issue_id: ${{ github.event.issue.number }} payload_marker: request fail_on_missing: false + - name: Check out request validator + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Validate requested repository + if: steps.issue_body_parser_request.outputs.payload != 'NOT_FOUND' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + REQUEST_PAYLOAD: ${{ steps.issue_body_parser_request.outputs.payload }} + with: + script: | + const { default: validateRequest } = await import('${{ github.workspace }}/.github/scripts/validate-request.mjs'); + validateRequest(process.env.REQUEST_PAYLOAD); initialize-request: runs-on: self-hosted needs: parse-issue From 024cfa1361f25f468f632a83af7e17dd76a8816f Mon Sep 17 00:00:00 2001 From: Rob Anderson Date: Tue, 15 Sep 2026 10:47:05 -0700 Subject: [PATCH 3/4] remove disallowed action --- .github/workflows/pr.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ff91da5..075ae2e 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -28,8 +28,3 @@ jobs: working-directory: ${{ env.working-directory }} - run: npm test working-directory: ${{ env.working-directory }} - - uses: tintef/nyc-reporter-action@66a40cea2d02fdb0ac8fa045a7355cbbf9b991a1 # 0.2.5 - with: - GITHUB_TOKEN: ${{ github.token }} - SKIP_COVERAGE_FOLDER: true - WORKING_DIRECTORY: ${{ env.working-directory }} From 7d40c20d8858046c361f149bd6af11ad23fd9e56 Mon Sep 17 00:00:00 2001 From: Rob Anderson Date: Tue, 15 Sep 2026 10:55:03 -0700 Subject: [PATCH 4/4] pin latest actions --- .github/workflows/pr.yml | 4 ++-- .github/workflows/test.yml | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 075ae2e..65f1c4a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,8 +17,8 @@ jobs: working-directory: ./.github/scripts steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 with: node-version: '20' check-latest: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4aad96b..0e30941 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ jobs: env: working-directory: ./.github/scripts steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v7.0.0 with: - node-version: 20 - cache: npm + node-version: '20' + check-latest: true + cache: 'npm' cache-dependency-path: ${{ env.working-directory }}/package-lock.json - run: npm ci working-directory: ${{ env.working-directory }}