Skip to content

Commit dacd705

Browse files
authored
chore: secure-proof workflows (#1105)
## What kind of change does this PR introduce? Proactive security hardening - implementing defense-in-depth for our preview release workflow. ## What is the current behavior? The current `preview-release.yml` workflow is **secure in practice** but uses a pattern that could be theoretically vulnerable if our existing safeguards were bypassed. Current workflow security analysis: - ✅ **Protected by maintainer-only label requirement** (`trigger: preview`) - ✅ **No code injection vulnerabilities** (no direct interpolation of user input) - ✅ **Limited permission scope** (only `pull-requests: write`) - ⚠️ **Theoretical risk**: Uses `pull_request_target` while checking out PR head code - ⚠️ **Pattern concern**: Executes `npm ci` and `npm run build` from forks in a context with secrets **Important**: Our workflow was never vulnerable to the attacks seen in the [recent incident](GHSA-cxm3-wv7p-598c) due to our security controls. However, in light of recent supply chain attacks, we're implementing additional layers of security. ## What is the new behavior? Implementing a **zero-trust architecture** that makes exploitation impossible even if all other safeguards fail. ### New Three-Workflow Architecture: 1. **`preview-build.yml`** - Executes untrusted fork code in a completely isolated environment (no secrets, minimal permissions) 2. **`trigger-tests.yml`** - Orchestrates testing using only artifacts (never touches fork code, has access to secrets) 3. **`preview-comment.yml`** - Updates PR status (read-only operations with artifacts) ### Security Improvements: | Security Layer | Previous (Secure) | New (Defense-in-Depth) | |---------------|-------------------|------------------------| | **Maintainer Control** | ✅ Required label | ✅ Required label | | **Code Injection Protection** | ✅ No interpolation | ✅ No interpolation | | **Fork Code Isolation** | ⚠️ Runs with secrets present | ✅ Complete isolation from secrets | | **Workflow Tampering** | ⚠️ Theoretical if branch protection bypassed | ✅ Impossible by design | | **Audit Trail** | ✅ GitHub logs | ✅ Enhanced with explicit PR author logging | | **Community Testing** | ⚠️ Requires trust | ✅ Safe by architecture | ### Key Architectural Benefits: - **Separation of Concerns**: Each workflow has a single, well-defined responsibility - **Artifact-Based Communication**: Data passes through GitHub's artifact system, not workflow contexts - **Fail-Safe Design**: Even if GitHub Actions introduced new vulnerabilities, our architecture would remain secure - **Industry Best Practice**: Aligns with GitHub Security Lab recommendations ## Additional context ### Why make this change now? Following the recent Nx supply chain attack and similar incidents in the ecosystem, we're proactively hardening our security posture. While our existing workflow **was not vulnerable** to the specific attack vectors used against Nx (thanks to our label requirements and lack of code injection points), we recognize that: 1. **Security is not binary** - Defense-in-depth provides resilience against unknown future attacks 2. **Patterns matter** - Even secure implementations of risky patterns can normalize their use 3. **Community confidence** - Demonstrating security leadership builds trust in Supabase's infrastructure ### Risk Assessment: - **Previous risk level**: Low (protected by multiple controls) - **New risk level**: Negligible (architecturally impossible to exploit) - **Implementation risk**: Minimal (preserves all existing functionality) ### Implementation Highlights: - ✅ Maintains full backwards compatibility - ✅ No changes required to existing workflows or processes - ✅ Actually improves functionality (safer community contributions) - ✅ Sets a security standard for other Supabase repositories - ✅ Provides a template for secure CI/CD in the broader ecosystem ### Technical Details: This change replaces a single 167-line workflow with three focused workflows (307 lines total) that: - Eliminate the use of `pull_request_target` with fork code execution - Ensure secrets are never available in the same context as untrusted code - Use GitHub's artifact system for secure inter-workflow communication - Add comprehensive logging for security auditing
1 parent be9a27c commit dacd705

File tree

4 files changed

+307
-167
lines changed

4 files changed

+307
-167
lines changed

.github/workflows/preview-build.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: Preview Build
2+
3+
permissions:
4+
contents: read
5+
pull-requests: read
6+
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, labeled]
10+
paths:
11+
- 'src/**'
12+
- 'package.json'
13+
- 'package-lock.json'
14+
- 'tsconfig.json'
15+
16+
jobs:
17+
build-preview:
18+
# Only run if PR has the 'trigger: preview' label, is on the correct repository, and targets master branch
19+
if: |
20+
github.repository == 'supabase/auth-js' &&
21+
github.event.pull_request.base.ref == 'master' &&
22+
contains(github.event.pull_request.labels.*.name, 'trigger: preview')
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 15
25+
outputs:
26+
preview-url: ${{ steps.preview.outputs.url }}
27+
pr-number: ${{ github.event.pull_request.number }}
28+
steps:
29+
# Checkout fork code - safe because no secrets are available
30+
- name: Checkout code
31+
uses: actions/checkout@v4
32+
33+
# Log PR author for auditing
34+
- name: Log PR author
35+
run: |
36+
echo "Preview build triggered by: ${{ github.event.pull_request.user.login }}"
37+
echo "PR #${{ github.event.pull_request.number }} from fork: ${{ github.event.pull_request.head.repo.full_name }}"
38+
39+
- name: Setup Node.js
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: '20'
43+
cache: 'npm'
44+
45+
- name: Install dependencies
46+
run: npm ci
47+
48+
- name: Build
49+
run: npm run build
50+
- name: Create preview release
51+
id: preview
52+
run: |
53+
echo "Creating preview release..."
54+
OUTPUT=$(npx pkg-pr-new@latest publish --compact 2>&1 || true)
55+
echo "Full output:"
56+
echo "$OUTPUT"
57+
58+
# Extract the preview URL
59+
PREVIEW_URL=$(echo "$OUTPUT" | grep -o 'https://pkg\.pr\.new/@supabase/[^[:space:]]*' | head -1)
60+
61+
if [ -z "$PREVIEW_URL" ]; then
62+
echo "Error: Failed to extract preview URL from pkg-pr-new output"
63+
echo "Output was: $OUTPUT"
64+
exit 1
65+
fi
66+
67+
echo "Preview Release URL: $PREVIEW_URL"
68+
echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
69+
70+
# Save preview info for the next workflows
71+
- name: Save preview info
72+
run: |
73+
mkdir -p preview-info
74+
echo "${{ steps.preview.outputs.url }}" > preview-info/preview-url.txt
75+
echo "${{ github.event.pull_request.number }}" > preview-info/pr-number.txt
76+
echo "${{ github.event.pull_request.head.sha }}" > preview-info/commit-sha.txt
77+
echo "auth-js" > preview-info/package-name.txt
78+
79+
- name: Upload preview info
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: preview-info
83+
path: preview-info/
84+
retention-days: 7

.github/workflows/preview-comment.yml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Update PR Comment
2+
3+
permissions:
4+
pull-requests: write
5+
actions: read
6+
7+
on:
8+
workflow_run:
9+
workflows: ["Preview Build", "Trigger Supabase JS Tests"]
10+
types: [completed]
11+
12+
jobs:
13+
update-comment:
14+
# Only run on the correct repository
15+
if: github.repository == 'supabase/auth-js'
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
steps:
19+
# Get PR number from the workflow run
20+
- name: Get PR info
21+
id: pr-info
22+
uses: actions/github-script@v7
23+
with:
24+
script: |
25+
// Get the workflow run details
26+
const workflowRun = context.payload.workflow_run;
27+
28+
// Find associated PR
29+
const prs = await github.rest.pulls.list({
30+
owner: context.repo.owner,
31+
repo: context.repo.repo,
32+
state: 'open',
33+
head: `${workflowRun.head_repository.owner.login}:${workflowRun.head_branch}`
34+
});
35+
36+
if (prs.data.length > 0) {
37+
const pr = prs.data[0];
38+
core.setOutput('pr-number', pr.number);
39+
core.setOutput('found', 'true');
40+
console.log(`Found PR #${pr.number}`);
41+
} else {
42+
core.setOutput('found', 'false');
43+
console.log('No associated PR found');
44+
}
45+
46+
# Only continue if we found a PR
47+
- name: Download preview info
48+
if: steps.pr-info.outputs.found == 'true' && github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'success'
49+
uses: actions/download-artifact@v4
50+
with:
51+
name: preview-info
52+
path: preview-info/
53+
run-id: ${{ github.event.workflow_run.id }}
54+
continue-on-error: true
55+
56+
- name: Read preview URL
57+
if: steps.pr-info.outputs.found == 'true' && github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'success'
58+
id: preview-url
59+
run: |
60+
if [ -f "preview-info/preview-url.txt" ]; then
61+
echo "url=$(cat preview-info/preview-url.txt)" >> $GITHUB_OUTPUT
62+
echo "found=true" >> $GITHUB_OUTPUT
63+
else
64+
echo "found=false" >> $GITHUB_OUTPUT
65+
fi
66+
continue-on-error: true
67+
68+
# Find existing comment
69+
- name: Find existing comment
70+
if: steps.pr-info.outputs.found == 'true'
71+
uses: peter-evans/find-comment@v3
72+
id: find-comment
73+
with:
74+
issue-number: ${{ steps.pr-info.outputs.pr-number }}
75+
comment-author: 'github-actions[bot]'
76+
body-includes: '<!-- auth-js-preview-status -->'
77+
78+
# Create or update comment based on workflow status
79+
- name: Create or update preview comment
80+
if: steps.pr-info.outputs.found == 'true'
81+
uses: peter-evans/create-or-update-comment@v4
82+
with:
83+
comment-id: ${{ steps.find-comment.outputs.comment-id }}
84+
issue-number: ${{ steps.pr-info.outputs.pr-number }}
85+
body: |
86+
<!-- auth-js-preview-status -->
87+
## 🚀 Preview Release Status
88+
89+
${{ github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'success' && steps.preview-url.outputs.found == 'true' && format('✅ **Preview package created successfully!**
90+
91+
📦 **Preview URL:** `{0}`
92+
93+
You can install this preview package in your project by running:
94+
```bash
95+
npm install {0}
96+
```
97+
98+
🔄 Supabase-js CI tests have been automatically triggered to verify compatibility.
99+
', steps.preview-url.outputs.url) || '' }}
100+
101+
${{ github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'failure' && '❌ **Preview build failed**
102+
103+
Please check the [workflow logs](' }}${{ github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.html_url || '' }}${{ github.event.workflow_run.name == 'Preview Build' && github.event.workflow_run.conclusion == 'failure' && ') for more details.' || '' }}
104+
105+
${{ github.event.workflow_run.name == 'Trigger Supabase JS Tests' && github.event.workflow_run.conclusion == 'success' && '✅ **Supabase-js tests triggered successfully!**
106+
107+
The integration tests are now running. Results will be posted here when complete.' || '' }}
108+
109+
${{ github.event.workflow_run.name == 'Trigger Supabase JS Tests' && github.event.workflow_run.conclusion == 'failure' && '⚠️ **Failed to trigger supabase-js tests**
110+
111+
The preview package was created but the integration tests could not be triggered. You may need to trigger them manually.' || '' }}
112+
113+
---
114+
<sub>Last updated: ${{ github.event.workflow_run.updated_at }}</sub>
115+
edit-mode: replace

.github/workflows/preview-release.yml

Lines changed: 0 additions & 167 deletions
This file was deleted.

0 commit comments

Comments
 (0)