From 213bc715815bf8e2f9796ae508a431d062df8bc9 Mon Sep 17 00:00:00 2001 From: merlin Date: Sat, 16 May 2026 15:08:46 +0800 Subject: [PATCH 1/5] security: harden workflow security based on audit findings - Add API allowlist to octo-ci-status/issue-feed/pr-feed to prevent OCTO_BOT_TOKEN from being sent to attacker-controlled URLs - Make workflow_id required in octo-ci-status (was optional, risking incorrect previous-run detection) - Add explicit sort for previous-run selection in octo-ci-status - Add sanitize_text() to issue-feed/pr-feed to prevent IM message injection - Add require_group_id() validation to issue-feed/pr-feed - Add timeout-minutes: 3 to issue-welcome job - Add payload guard and idempotency marker to issue-welcome script Audit performed by codex (gpt-5.5), fixes applied by claude-opus-4-6. --- .github/workflows/issue-welcome.yml | 18 ++++++++++++++- .github/workflows/octo-ci-status.yml | 26 +++++++++++++++------- .github/workflows/octo-issue-feed.yml | 30 +++++++++++++++++++++---- .github/workflows/octo-pr-feed.yml | 32 ++++++++++++++++++++++----- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/.github/workflows/issue-welcome.yml b/.github/workflows/issue-welcome.yml index fa7c474..df9753e 100644 --- a/.github/workflows/issue-welcome.yml +++ b/.github/workflows/issue-welcome.yml @@ -6,6 +6,7 @@ on: jobs: welcome: runs-on: ubuntu-latest + timeout-minutes: 3 permissions: issues: write steps: @@ -15,6 +16,21 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const issue = context.payload.issue; + if (!issue) { + core.setFailed('This workflow must be called from an issues event. context.payload.issue is undefined.'); + return; + } + const marker = ''; + const existingComments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + per_page: 100 + }); + if (existingComments.some(c => c.user.type === 'Bot' && c.body && c.body.includes(marker))) { + console.log('Welcome comment already posted, skipping (idempotent).'); + return; + } // Skip bot-opened issues (Dependabot, etc.) if (issue.user.type === 'Bot') { @@ -113,5 +129,5 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, - body + body: marker + '\n' + body }); diff --git a/.github/workflows/octo-ci-status.yml b/.github/workflows/octo-ci-status.yml index 579f4d6..ccd6bb8 100644 --- a/.github/workflows/octo-ci-status.yml +++ b/.github/workflows/octo-ci-status.yml @@ -20,9 +20,8 @@ on: default: 0 workflow_id: type: number - required: false - default: 0 - description: 'Numeric workflow ID (strongly recommended). Without it, run history is fetched globally for the repo and may include other workflows. Pass ${{ github.workflow_run.workflow_id }} from caller.' + required: true + description: 'Numeric workflow ID. Pass ${{ github.event.workflow_run.workflow_id }} from caller.' run_url: type: string required: true @@ -139,10 +138,14 @@ jobs: curr_created = current['created_at'] # Previous: any run older than current (by created_at), same workflow name - older = [r for r in all_runs - if r['id'] != current['id'] - and r['name'] == wf_name # defensive; redundant when workflow_id is set (already scoped) - and r['created_at'] < curr_created] + older = sorted( + [r for r in all_runs + if r['id'] != current['id'] + and r['name'] == wf_name # defensive; redundant when workflow_id is set (already scoped) + and r['created_at'] < curr_created], + key=lambda r: r['created_at'], + reverse=True, + ) previous = older[0] if older else None curr_conclusion = current['conclusion'] @@ -186,7 +189,14 @@ jobs: sys.exit(0) # Send to Octo IM - send_url = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage' + ALLOWED_API_BASES = { + 'https://im.deepminer.com.cn/api', + } + _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + if _api_base not in ALLOWED_API_BASES: + print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') + sys.exit(2) + send_url = _api_base + '/v1/bot/sendMessage' headers = { 'Authorization': f'Bearer {bot_token}', 'Content-Type': 'application/json', diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index e4edc05..33e9283 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -70,6 +70,21 @@ jobs: return val + def sanitize_text(s, max_len=300): + s = str(s or '') + s = s.replace('\r', ' ').replace('\n', ' ') + return s[:max_len] + + + import re + def require_group_id(name): + val = require_env(name) + if not re.fullmatch(r'[0-9a-f]{32}', val): + print(f'ERROR: {name} must be a 32-char lowercase hex group id') + sys.exit(2) + return val + + action = require_env('EVENT_ACTION') emoji = {'opened': '๐Ÿ†•', 'closed': 'โœ…', 'reopened': '๐Ÿ”„', 'labeled': '๐Ÿท๏ธ'}.get(action, 'โ„น๏ธ') @@ -81,14 +96,21 @@ jobs: repo = require_env('REPO_NAME') num = require_env('ISSUE_NUMBER') - title = require_env('ISSUE_TITLE') + title = sanitize_text(require_env('ISSUE_TITLE'), max_len=300) url = require_env('ISSUE_URL') - author = require_env('ISSUE_AUTHOR') + author = sanitize_text(require_env('ISSUE_AUTHOR'), max_len=80) feed_msg = f"{emoji} [{repo}] Issue #{num} ยท {title}\n๐Ÿ‘ค {author}{labels_part}\n๐Ÿ”— {url}" proj_msg = f"{emoji} Issue #{num} ยท {title}\n๐Ÿ‘ค {author}{labels_part}\n๐Ÿ”— {url}" - api = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage' + ALLOWED_API_BASES = { + 'https://im.deepminer.com.cn/api', + } + _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + if _api_base not in ALLOWED_API_BASES: + print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') + sys.exit(2) + api = _api_base + '/v1/bot/sendMessage' token = require_env('OCTO_BOT_TOKEN') headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} @@ -133,7 +155,7 @@ jobs: print(f'ERROR: failed to send message to {group_id[:8]}...: {last_err}') failed.append(group_id) - proj_gid = require_env('PROJECT_GROUP_ID') + proj_gid = require_group_id('PROJECT_GROUP_ID') send('151a45970e1546afa9e947ac36a5c4e5', feed_msg) send(proj_gid, proj_msg) diff --git a/.github/workflows/octo-pr-feed.yml b/.github/workflows/octo-pr-feed.yml index 92e6c56..86ae9fe 100644 --- a/.github/workflows/octo-pr-feed.yml +++ b/.github/workflows/octo-pr-feed.yml @@ -87,6 +87,21 @@ jobs: return val + def sanitize_text(s, max_len=300): + s = str(s or '') + s = s.replace('\r', ' ').replace('\n', ' ') + return s[:max_len] + + + import re + def require_group_id(name): + val = require_env(name) + if not re.fullmatch(r'[0-9a-f]{32}', val): + print(f'ERROR: {name} must be a 32-char lowercase hex group id') + sys.exit(2) + return val + + action = require_env('EVENT_ACTION') merged = require_env('PR_MERGED').lower() == 'true' @@ -103,14 +118,21 @@ jobs: repo = require_env('REPO_NAME') num = require_env('PR_NUMBER') - title = require_env('PR_TITLE') + title = sanitize_text(require_env('PR_TITLE'), max_len=300) url = require_env('PR_URL') - author = require_env('PR_AUTHOR') + author = sanitize_text(require_env('PR_AUTHOR'), max_len=80) feed_msg = f"{emoji} [{repo}] PR #{num} ยท {title}\n๐Ÿ‘ค {author}{stats_part}\n๐Ÿ”— {url}" proj_msg = f"{emoji} PR #{num} ยท {title}\n๐Ÿ‘ค {author}{stats_part}\n๐Ÿ”— {url}" - api = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + '/v1/bot/sendMessage' + ALLOWED_API_BASES = { + 'https://im.deepminer.com.cn/api', + } + _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') + if _api_base not in ALLOWED_API_BASES: + print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') + sys.exit(2) + api = _api_base + '/v1/bot/sendMessage' token = require_env('OCTO_BOT_TOKEN') headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'} @@ -155,8 +177,8 @@ jobs: print(f'ERROR: failed to send message to {group_id[:8]}...: {last_err}') failed.append(group_id) - feed_gid = require_env('FEED_GROUP_ID') - proj_gid = require_env('PROJECT_GROUP_ID') + feed_gid = require_group_id('FEED_GROUP_ID') + proj_gid = require_group_id('PROJECT_GROUP_ID') send(feed_gid, feed_msg) send(proj_gid, proj_msg) if failed: From 07ef8ee9f64f9da2e5d334f8e6b7b7360e0b025d Mon Sep 17 00:00:00 2001 From: merlin Date: Sat, 16 May 2026 16:45:15 +0800 Subject: [PATCH 2/5] fix: move import re to top-level imports in issue-feed and pr-feed workflows Per code review (P2): merge standalone 'import re' into the top-level import line in both octo-issue-feed.yml and octo-pr-feed.yml. Before: import re appeared mid-script between function definitions After: import os, json, re, sys, time, urllib.request, urllib.error --- .github/workflows/octo-issue-feed.yml | 3 +-- .github/workflows/octo-pr-feed.yml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index 33e9283..4df9fe8 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -59,7 +59,7 @@ jobs: API_BASE_URL: ${{ inputs.api_base_url }} run: | python3 - << 'PYEOF' - import os, json, sys, time, urllib.request, urllib.error + import os, json, re, sys, time, urllib.request, urllib.error def require_env(name): @@ -76,7 +76,6 @@ jobs: return s[:max_len] - import re def require_group_id(name): val = require_env(name) if not re.fullmatch(r'[0-9a-f]{32}', val): diff --git a/.github/workflows/octo-pr-feed.yml b/.github/workflows/octo-pr-feed.yml index 86ae9fe..5571651 100644 --- a/.github/workflows/octo-pr-feed.yml +++ b/.github/workflows/octo-pr-feed.yml @@ -76,7 +76,7 @@ jobs: API_BASE_URL: ${{ inputs.api_base_url }} run: | python3 - << 'PYEOF' - import os, json, sys, time, urllib.request, urllib.error + import os, json, re, sys, time, urllib.request, urllib.error def require_env(name): @@ -93,7 +93,6 @@ jobs: return s[:max_len] - import re def require_group_id(name): val = require_env(name) if not re.fullmatch(r'[0-9a-f]{32}', val): From c0a8185af47dd466a2260f9abc0240ebf5088619 Mon Sep 17 00:00:00 2001 From: merlin Date: Sat, 16 May 2026 16:52:45 +0800 Subject: [PATCH 3/5] fix: address review observations - labels sanitize, bot check order, group_id validation - octo-issue-feed.yml: sanitize each label via sanitize_text(l, max_len=64) to prevent newline injection in label names (Obs #2) - issue-welcome.yml: move bot-type check before paginate() to avoid unnecessary API round-trips for bot-opened issues (Obs #3) - octo-ci-status.yml: add require_group_id() + re import, replace require_env('PROJECT_GROUP_ID') with require_group_id() for consistency with issue-feed and pr-feed (Obs #5) --- .github/workflows/issue-welcome.yml | 12 ++++++------ .github/workflows/octo-ci-status.yml | 12 ++++++++++-- .github/workflows/octo-issue-feed.yml | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/issue-welcome.yml b/.github/workflows/issue-welcome.yml index df9753e..0fe3ad2 100644 --- a/.github/workflows/issue-welcome.yml +++ b/.github/workflows/issue-welcome.yml @@ -20,6 +20,12 @@ jobs: core.setFailed('This workflow must be called from an issues event. context.payload.issue is undefined.'); return; } + // Skip bot-opened issues (Dependabot, etc.) + if (issue.user.type === 'Bot') { + console.log('Skipping bot-opened issue'); + return; + } + const marker = ''; const existingComments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, @@ -32,12 +38,6 @@ jobs: return; } - // Skip bot-opened issues (Dependabot, etc.) - if (issue.user.type === 'Bot') { - console.log('Skipping bot-opened issue'); - return; - } - // Language detection: Chinese characters in title or body โ†’ reply in Chinese const text = (issue.title || '') + ' ' + (issue.body || ''); const isChinese = /[\u4e00-\u9fff]/.test(text); diff --git a/.github/workflows/octo-ci-status.yml b/.github/workflows/octo-ci-status.yml index ccd6bb8..3bf0092 100644 --- a/.github/workflows/octo-ci-status.yml +++ b/.github/workflows/octo-ci-status.yml @@ -59,7 +59,7 @@ jobs: API_BASE_URL: ${{ inputs.api_base_url }} run: | python3 - << 'PYEOF' - import os, json, time, urllib.request, urllib.error, sys + import os, json, re, time, urllib.request, urllib.error, sys def require_env(name): @@ -68,6 +68,14 @@ jobs: print(f'ERROR: required environment variable {name} is missing or empty') sys.exit(2) return val + + + def require_group_id(name): + val = require_env(name) + if not re.fullmatch(r'[0-9a-f]{32}', val): + print(f'ERROR: {name} must be a 32-char lowercase hex group id') + sys.exit(2) + return val conclusion = require_env('CONCLUSION') @@ -79,7 +87,7 @@ jobs: repo = require_env('REPO_NAME') wf_name = require_env('WORKFLOW_NAME') run_url = require_env('RUN_URL') - proj_gid = require_env('PROJECT_GROUP_ID') + proj_gid = require_group_id('PROJECT_GROUP_ID') gh_token = require_env('GITHUB_TOKEN') bot_token = require_env('OCTO_BOT_TOKEN') diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index 4df9fe8..7747c91 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -88,7 +88,7 @@ jobs: emoji = {'opened': '๐Ÿ†•', 'closed': 'โœ…', 'reopened': '๐Ÿ”„', 'labeled': '๐Ÿท๏ธ'}.get(action, 'โ„น๏ธ') try: - labels = json.loads(os.environ.get('ISSUE_LABELS', '[]')) + labels = [sanitize_text(l, max_len=64) for l in json.loads(os.environ.get('ISSUE_LABELS', '[]'))] labels_part = ' ยท ๐Ÿท๏ธ ' + ', '.join(labels) if labels else '' except Exception: labels_part = '' From 2be485ae8fbf0b3d69578b254006a403806fcf8d Mon Sep 17 00:00:00 2001 From: lml2468 Date: Sat, 16 May 2026 17:32:55 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20drop?= =?UTF-8?q?=20Bot-type=20filter,=20add=20concurrency,=20clarify=20api=5Fba?= =?UTF-8?q?se=5Furl,=20hoist=20proj=5Fgid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-welcome.yml | 5 ++++- .github/workflows/octo-ci-status.yml | 2 +- .github/workflows/octo-issue-feed.yml | 4 ++-- .github/workflows/octo-pr-feed.yml | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/issue-welcome.yml b/.github/workflows/issue-welcome.yml index 0fe3ad2..e40c11a 100644 --- a/.github/workflows/issue-welcome.yml +++ b/.github/workflows/issue-welcome.yml @@ -7,6 +7,9 @@ jobs: welcome: runs-on: ubuntu-latest timeout-minutes: 3 + concurrency: + group: welcome-${{ github.event.issue.number }} + cancel-in-progress: false permissions: issues: write steps: @@ -33,7 +36,7 @@ jobs: issue_number: issue.number, per_page: 100 }); - if (existingComments.some(c => c.user.type === 'Bot' && c.body && c.body.includes(marker))) { + if (existingComments.some(c => c.body && c.body.includes(marker))) { console.log('Welcome comment already posted, skipping (idempotent).'); return; } diff --git a/.github/workflows/octo-ci-status.yml b/.github/workflows/octo-ci-status.yml index 3bf0092..a70693e 100644 --- a/.github/workflows/octo-ci-status.yml +++ b/.github/workflows/octo-ci-status.yml @@ -32,7 +32,7 @@ on: type: string required: false default: 'https://im.deepminer.com.cn/api' - description: 'Octo IM API base URL (without trailing slash)' + description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.' secrets: OCTO_BOT_TOKEN: required: true diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index 7747c91..c11c94b 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -33,7 +33,7 @@ on: type: string required: false default: 'https://im.deepminer.com.cn/api' - description: 'Octo IM API base URL (without trailing slash)' + description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.' secrets: OCTO_BOT_TOKEN: required: true @@ -98,6 +98,7 @@ jobs: title = sanitize_text(require_env('ISSUE_TITLE'), max_len=300) url = require_env('ISSUE_URL') author = sanitize_text(require_env('ISSUE_AUTHOR'), max_len=80) + proj_gid = require_group_id('PROJECT_GROUP_ID') feed_msg = f"{emoji} [{repo}] Issue #{num} ยท {title}\n๐Ÿ‘ค {author}{labels_part}\n๐Ÿ”— {url}" proj_msg = f"{emoji} Issue #{num} ยท {title}\n๐Ÿ‘ค {author}{labels_part}\n๐Ÿ”— {url}" @@ -154,7 +155,6 @@ jobs: print(f'ERROR: failed to send message to {group_id[:8]}...: {last_err}') failed.append(group_id) - proj_gid = require_group_id('PROJECT_GROUP_ID') send('151a45970e1546afa9e947ac36a5c4e5', feed_msg) send(proj_gid, proj_msg) diff --git a/.github/workflows/octo-pr-feed.yml b/.github/workflows/octo-pr-feed.yml index 5571651..4dc3a91 100644 --- a/.github/workflows/octo-pr-feed.yml +++ b/.github/workflows/octo-pr-feed.yml @@ -42,7 +42,7 @@ on: type: string required: false default: 'https://im.deepminer.com.cn/api' - description: 'Octo IM API base URL (without trailing slash)' + description: 'Octo IM API base URL. Only the production endpoint is allowlisted; any other value will cause the workflow to fail.' feed_group_id: type: string required: true From fc8bdb867289a7c41197e207a0301fbf5f8c7d55 Mon Sep 17 00:00:00 2001 From: wangdachui-bot Date: Sun, 17 May 2026 21:30:23 +0800 Subject: [PATCH 5/5] fix: address remaining P2 review feedback - Add require_repo_name() validation to prevent path traversal in GitHub API URL construction (all 3 notification workflows) - Harden sanitize_text() to strip all C0 control characters (U+0000-U+001F) and DEL, not just CR/LF (issue-feed, pr-feed) - Make API allowlist comparison case-insensitive per RFC 3986 hostname semantics (all 3 notification workflows) - Add docstrings to require_group_id() clarifying it validates format only, not authorization (all 3 notification workflows) - Add safety comment on repo interpolation in triage URL Addresses review feedback from yujiawei across 4 review rounds. --- .github/workflows/octo-ci-status.yml | 14 ++++++++++++-- .github/workflows/octo-issue-feed.yml | 19 ++++++++++++++++--- .github/workflows/octo-pr-feed.yml | 18 +++++++++++++++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.github/workflows/octo-ci-status.yml b/.github/workflows/octo-ci-status.yml index a70693e..f7f94b0 100644 --- a/.github/workflows/octo-ci-status.yml +++ b/.github/workflows/octo-ci-status.yml @@ -71,11 +71,21 @@ jobs: def require_group_id(name): + """Validate format only (32-char hex); this is not an authorization check.""" val = require_env(name) if not re.fullmatch(r'[0-9a-f]{32}', val): print(f'ERROR: {name} must be a 32-char lowercase hex group id') sys.exit(2) return val + + + def require_repo_name(name): + """Validate repo name to prevent path traversal in GitHub API URLs.""" + val = require_env(name) + if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val): + print(f'ERROR: {name} contains invalid characters: {val!r}') + sys.exit(2) + return val conclusion = require_env('CONCLUSION') @@ -84,7 +94,7 @@ jobs: print('Cancelled run, skipping.') sys.exit(0) - repo = require_env('REPO_NAME') + repo = require_repo_name('REPO_NAME') wf_name = require_env('WORKFLOW_NAME') run_url = require_env('RUN_URL') proj_gid = require_group_id('PROJECT_GROUP_ID') @@ -201,7 +211,7 @@ jobs: 'https://im.deepminer.com.cn/api', } _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') - if _api_base not in ALLOWED_API_BASES: + if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}: print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') sys.exit(2) send_url = _api_base + '/v1/bot/sendMessage' diff --git a/.github/workflows/octo-issue-feed.yml b/.github/workflows/octo-issue-feed.yml index c11c94b..b4a9f5d 100644 --- a/.github/workflows/octo-issue-feed.yml +++ b/.github/workflows/octo-issue-feed.yml @@ -71,17 +71,29 @@ jobs: def sanitize_text(s, max_len=300): + """Strip control characters (CR, LF, tabs, etc.) to prevent IM message injection.""" s = str(s or '') - s = s.replace('\r', ' ').replace('\n', ' ') + # Strip all C0 control characters (U+0000-U+001F) and DEL (U+007F) + s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) return s[:max_len] def require_group_id(name): + """Validate format only (32-char hex); this is not an authorization check.""" val = require_env(name) if not re.fullmatch(r'[0-9a-f]{32}', val): print(f'ERROR: {name} must be a 32-char lowercase hex group id') sys.exit(2) return val + + + def require_repo_name(name): + """Validate repo name to prevent path traversal in GitHub API URLs.""" + val = require_env(name) + if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val): + print(f'ERROR: {name} contains invalid characters: {val!r}') + sys.exit(2) + return val action = require_env('EVENT_ACTION') @@ -93,7 +105,7 @@ jobs: except Exception: labels_part = '' - repo = require_env('REPO_NAME') + repo = require_repo_name('REPO_NAME') num = require_env('ISSUE_NUMBER') title = sanitize_text(require_env('ISSUE_TITLE'), max_len=300) url = require_env('ISSUE_URL') @@ -107,7 +119,7 @@ jobs: 'https://im.deepminer.com.cn/api', } _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') - if _api_base not in ALLOWED_API_BASES: + if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}: print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') sys.exit(2) api = _api_base + '/v1/bot/sendMessage' @@ -160,6 +172,7 @@ jobs: # Trigger IssueTriage bot on newly opened issues if action == 'opened': + # repo is already validated by require_repo_name; safe to interpolate into URL triage_msg = ( '@[27pmzxx8nad78c9d01e_bot:Octo ๅŠฉ็†-IssueTriage] [TRIAGE] ' f'https://github.com/Mininglamp-OSS/{repo}/issues/{num}' diff --git a/.github/workflows/octo-pr-feed.yml b/.github/workflows/octo-pr-feed.yml index 4dc3a91..75afaa3 100644 --- a/.github/workflows/octo-pr-feed.yml +++ b/.github/workflows/octo-pr-feed.yml @@ -88,17 +88,29 @@ jobs: def sanitize_text(s, max_len=300): + """Strip control characters (CR, LF, tabs, etc.) to prevent IM message injection.""" s = str(s or '') - s = s.replace('\r', ' ').replace('\n', ' ') + # Strip all C0 control characters (U+0000-U+001F) and DEL (U+007F) + s = re.sub(r'[\x00-\x1f\x7f]', ' ', s) return s[:max_len] def require_group_id(name): + """Validate format only (32-char hex); this is not an authorization check.""" val = require_env(name) if not re.fullmatch(r'[0-9a-f]{32}', val): print(f'ERROR: {name} must be a 32-char lowercase hex group id') sys.exit(2) return val + + + def require_repo_name(name): + """Validate repo name to prevent path traversal in GitHub API URLs.""" + val = require_env(name) + if not re.fullmatch(r'[A-Za-z0-9._-]{1,100}', val): + print(f'ERROR: {name} contains invalid characters: {val!r}') + sys.exit(2) + return val action = require_env('EVENT_ACTION') @@ -115,7 +127,7 @@ jobs: files = int(os.environ.get('PR_CHANGED_FILES', 0) or 0) stats_part = f' ยท +{adds} -{dels} ยท {files} files' if (adds or dels or files) else '' - repo = require_env('REPO_NAME') + repo = require_repo_name('REPO_NAME') num = require_env('PR_NUMBER') title = sanitize_text(require_env('PR_TITLE'), max_len=300) url = require_env('PR_URL') @@ -128,7 +140,7 @@ jobs: 'https://im.deepminer.com.cn/api', } _api_base = os.environ.get('API_BASE_URL', 'https://im.deepminer.com.cn/api').rstrip('/') - if _api_base not in ALLOWED_API_BASES: + if _api_base.lower() not in {b.lower() for b in ALLOWED_API_BASES}: print(f'ERROR: API_BASE_URL is not in the allowlist: {_api_base}') sys.exit(2) api = _api_base + '/v1/bot/sendMessage'