validate git repo urls and refs, require a secret for webhook triggers, tidy internal endpoint comments
This commit is contained in:
10 files changed
+186
-91
No files matched your search
@@ -68,6 +68,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
return json({ error: 'A stack with this name already exists on this environment' }, { status: 409 });
|
||||
}
|
||||
|
||||
// A secret is mandatory when the webhook is enabled.
|
||||
if (data.webhookEnabled && !data.webhookSecret?.trim()) {
|
||||
return json({ error: 'A webhook secret is required when the webhook is enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Either repositoryId or new repo details (url, branch) must be provided
|
||||
let repositoryId = data.repositoryId;
|
||||
|
||||
|
||||
@@ -63,6 +63,14 @@ export const PUT: RequestHandler = async (event) => {
|
||||
data.stackName = trimmedStackName;
|
||||
}
|
||||
|
||||
// A secret is mandatory when the webhook is enabled.
|
||||
// Evaluate the effective post-update state (PUT is partial).
|
||||
const effWebhookEnabled = data.webhookEnabled !== undefined ? data.webhookEnabled : existing.webhookEnabled;
|
||||
const effWebhookSecret = data.webhookSecret !== undefined ? data.webhookSecret : existing.webhookSecret;
|
||||
if (effWebhookEnabled && !effWebhookSecret?.trim()) {
|
||||
return json({ error: 'A webhook secret is required when the webhook is enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
const oldStackName = existing.stackName;
|
||||
const updated = await updateGitStack(id, {
|
||||
stackName: data.stackName,
|
||||
@@ -195,8 +203,10 @@ export const DELETE: RequestHandler = async (event) => {
|
||||
// Delete the stack_sources record to free up the stack name
|
||||
await deleteStackSource(existing.stackName, existing.environmentId);
|
||||
|
||||
// Delete all env var overrides for this stack (all environments)
|
||||
await deleteStackEnvVars(existing.stackName);
|
||||
// Delete this stack's env var overrides ON THIS ENVIRONMENT ONLY. The same
|
||||
// stack name can be deployed to multiple environments independently (per-env
|
||||
// rows), so an unscoped delete would wipe another environment's vars/secrets.
|
||||
await deleteStackEnvVars(existing.stackName, existing.environmentId);
|
||||
|
||||
// Delete from database
|
||||
await deleteGitStack(id);
|
||||
|
||||
@@ -3,29 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { getGitStack } from '$lib/server/db';
|
||||
import { deployGitStack } from '$lib/server/git';
|
||||
import { auditGitStack } from '$lib/server/audit';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
function verifySignature(payload: string, signature: string | null, secret: string): boolean {
|
||||
if (!signature) return false;
|
||||
|
||||
// Support both GitHub and GitLab webhook signatures
|
||||
// GitHub: sha256=<hash>
|
||||
// GitLab: just the token value in X-Gitlab-Token header
|
||||
|
||||
if (signature.startsWith('sha256=')) {
|
||||
const expectedSignature = 'sha256=' + crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
const sigBuf = Buffer.from(signature);
|
||||
const expectedBuf = Buffer.from(expectedSignature);
|
||||
if (sigBuf.length !== expectedBuf.length) return false;
|
||||
return crypto.timingSafeEqual(sigBuf, expectedBuf);
|
||||
}
|
||||
|
||||
// GitLab uses X-Gitlab-Token which should match exactly
|
||||
return signature === secret;
|
||||
}
|
||||
import { verifyWebhookSignature } from '$lib/server/webhook-signature';
|
||||
|
||||
function detectSource(request: Request): string {
|
||||
if (request.headers.get('x-hub-signature-256')) return 'github';
|
||||
@@ -52,20 +30,25 @@ export const POST: RequestHandler = async (event) => {
|
||||
|
||||
const source = detectSource(request);
|
||||
|
||||
// Verify webhook secret if set
|
||||
if (gitStack.webhookSecret) {
|
||||
const payload = await request.text();
|
||||
const githubSignature = request.headers.get('x-hub-signature-256');
|
||||
const gitlabToken = request.headers.get('x-gitlab-token');
|
||||
// A secret is mandatory: reject if none is configured.
|
||||
if (!gitStack.webhookSecret) {
|
||||
await auditGitStack(event, 'webhook', id, gitStack.stackName, gitStack.environmentId, {
|
||||
method: 'POST', source, error: 'no_secret_configured'
|
||||
});
|
||||
return json({ error: 'Webhook secret is not configured for this stack' }, { status: 401 });
|
||||
}
|
||||
|
||||
const signature = githubSignature || gitlabToken;
|
||||
const payload = await request.text();
|
||||
const githubSignature = request.headers.get('x-hub-signature-256');
|
||||
const gitlabToken = request.headers.get('x-gitlab-token');
|
||||
|
||||
if (!verifySignature(payload, signature, gitStack.webhookSecret)) {
|
||||
await auditGitStack(event, 'webhook', id, gitStack.stackName, gitStack.environmentId, {
|
||||
method: 'POST', source, error: 'invalid_signature'
|
||||
});
|
||||
return json({ error: 'Invalid webhook signature' }, { status: 401 });
|
||||
}
|
||||
const signature = githubSignature || gitlabToken;
|
||||
|
||||
if (!verifyWebhookSignature(payload, signature, gitStack.webhookSecret)) {
|
||||
await auditGitStack(event, 'webhook', id, gitStack.stackName, gitStack.environmentId, {
|
||||
method: 'POST', source, error: 'invalid_signature'
|
||||
});
|
||||
return json({ error: 'Invalid webhook signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Deploy the git stack (syncs and deploys only if there are changes)
|
||||
@@ -98,9 +81,17 @@ export const GET: RequestHandler = async (event) => {
|
||||
return json({ error: 'Webhook is not enabled for this stack' }, { status: 403 });
|
||||
}
|
||||
|
||||
// A secret is mandatory (see POST handler). Reject if none is configured.
|
||||
if (!gitStack.webhookSecret) {
|
||||
await auditGitStack(event, 'webhook', id, gitStack.stackName, gitStack.environmentId, {
|
||||
method: 'GET', source: 'get', error: 'no_secret_configured'
|
||||
});
|
||||
return json({ error: 'Webhook secret is not configured for this stack' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verify secret via query parameter for GET requests
|
||||
const secret = url.searchParams.get('secret');
|
||||
if (gitStack.webhookSecret && secret !== gitStack.webhookSecret) {
|
||||
if (secret !== gitStack.webhookSecret) {
|
||||
await auditGitStack(event, 'webhook', id, gitStack.stackName, gitStack.environmentId, {
|
||||
method: 'GET', source: 'get', error: 'invalid_secret'
|
||||
});
|
||||
|
||||
@@ -3,29 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { getGitRepository } from '$lib/server/db';
|
||||
import { deployFromRepository } from '$lib/server/git';
|
||||
import { auditGitRepository } from '$lib/server/audit';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
function verifySignature(payload: string, signature: string | null, secret: string): boolean {
|
||||
if (!signature) return false;
|
||||
|
||||
// Support both GitHub and GitLab webhook signatures
|
||||
// GitHub: sha256=<hash>
|
||||
// GitLab: just the token value in X-Gitlab-Token header
|
||||
|
||||
if (signature.startsWith('sha256=')) {
|
||||
const expectedSignature = 'sha256=' + crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
const sigBuf = Buffer.from(signature);
|
||||
const expectedBuf = Buffer.from(expectedSignature);
|
||||
if (sigBuf.length !== expectedBuf.length) return false;
|
||||
return crypto.timingSafeEqual(sigBuf, expectedBuf);
|
||||
}
|
||||
|
||||
// GitLab uses X-Gitlab-Token which should match exactly
|
||||
return signature === secret;
|
||||
}
|
||||
import { verifyWebhookSignature } from '$lib/server/webhook-signature';
|
||||
|
||||
function detectSource(request: Request): string {
|
||||
if (request.headers.get('x-hub-signature-256')) return 'github';
|
||||
@@ -52,20 +30,25 @@ export const POST: RequestHandler = async (event) => {
|
||||
|
||||
const source = detectSource(request);
|
||||
|
||||
// Verify webhook secret if set
|
||||
if (repository.webhookSecret) {
|
||||
const payload = await request.text();
|
||||
const githubSignature = request.headers.get('x-hub-signature-256');
|
||||
const gitlabToken = request.headers.get('x-gitlab-token');
|
||||
// A secret is mandatory: reject if none is configured.
|
||||
if (!repository.webhookSecret) {
|
||||
await auditGitRepository(event, 'webhook', id, repository.name, {
|
||||
method: 'POST', source, error: 'no_secret_configured'
|
||||
});
|
||||
return json({ error: 'Webhook secret is not configured for this repository' }, { status: 401 });
|
||||
}
|
||||
|
||||
const signature = githubSignature || gitlabToken;
|
||||
const payload = await request.text();
|
||||
const githubSignature = request.headers.get('x-hub-signature-256');
|
||||
const gitlabToken = request.headers.get('x-gitlab-token');
|
||||
|
||||
if (!verifySignature(payload, signature, repository.webhookSecret)) {
|
||||
await auditGitRepository(event, 'webhook', id, repository.name, {
|
||||
method: 'POST', source, error: 'invalid_signature'
|
||||
});
|
||||
return json({ error: 'Invalid webhook signature' }, { status: 401 });
|
||||
}
|
||||
const signature = githubSignature || gitlabToken;
|
||||
|
||||
if (!verifyWebhookSignature(payload, signature, repository.webhookSecret)) {
|
||||
await auditGitRepository(event, 'webhook', id, repository.name, {
|
||||
method: 'POST', source, error: 'invalid_signature'
|
||||
});
|
||||
return json({ error: 'Invalid webhook signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Optionally check which branch was pushed (for GitHub)
|
||||
@@ -104,9 +87,17 @@ export const GET: RequestHandler = async (event) => {
|
||||
return json({ error: 'Webhook is not enabled for this repository' }, { status: 403 });
|
||||
}
|
||||
|
||||
// A secret is mandatory (see POST handler). Reject if none is configured.
|
||||
if (!repository.webhookSecret) {
|
||||
await auditGitRepository(event, 'webhook', id, repository.name, {
|
||||
method: 'GET', source: 'get', error: 'no_secret_configured'
|
||||
});
|
||||
return json({ error: 'Webhook secret is not configured for this repository' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verify secret via query parameter for GET requests
|
||||
const secret = url.searchParams.get('secret');
|
||||
if (repository.webhookSecret && secret !== repository.webhookSecret) {
|
||||
if (secret !== repository.webhookSecret) {
|
||||
await auditGitRepository(event, 'webhook', id, repository.name, {
|
||||
method: 'GET', source: 'get', error: 'invalid_secret'
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { RequestHandler } from './$types';
|
||||
* GET /api/jobs/[id]
|
||||
* Poll a job's status and accumulated lines.
|
||||
* Returns all lines every time — client tracks its own cursor locally.
|
||||
* No auth required: job IDs are UUIDs (unguessable), no sensitive data beyond what the initiating user triggered.
|
||||
*/
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const job = getJob(params.id);
|
||||
|
||||
Reference in new issue
Block a user