Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions admin/modules/v1/controllers/XeroWebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Yii;
use yii\filters\Cors;
use yii\rest\Controller;
use yii\web\UnauthorizedHttpException;

class XeroWebhookController extends Controller
{
Expand Down Expand Up @@ -62,23 +63,24 @@ public function beforeAction($action)
return false;
}

$key = "+E4OxefKZm8uPKkiz8RkGA8t/XogInNgvIZhX9izCliOMVCerc8114/T7JSxudGfPxfwU1N3UCe1Ika2VuKDbQ==";
$key = getenv('XERO_WEBHOOK_SIGNING_KEY') ?: '';
if ($key === '') {
Yii::error('XERO_WEBHOOK_SIGNING_KEY is not configured.', __METHOD__);
throw new UnauthorizedHttpException('Webhook signature validation is not configured.');
}

// Get the provided signature from the request headers
$provided_signature = Yii::$app->request->headers->get("x-xero-signature"); // $_SERVER['HTTP_X_Xero_Signature'];

// Get the request data
$request_data = Yii::$app->request->getBodyParams(); //file_get_contents('php://input');
$request_data = Yii::$app->request->rawBody;

// Calculate the HMAC signature
$generated_signature = base64_encode(hash_hmac('sha256', $request_data, $key, true));

// Compare the provided signature with the generated one
if ($provided_signature !== $generated_signature) {
// Signature mismatch
http_response_code(401);
echo "Signature mismatch";
die();
if (!$provided_signature || !hash_equals($generated_signature, $provided_signature)) {
throw new UnauthorizedHttpException('Signature mismatch.');
} /*else {
// Signature matched
http_response_code(200);
Expand Down Expand Up @@ -106,4 +108,4 @@ public function actionIncomming()
Yii::$app->eventManager->track ($event['eventCategory'] . " " . $event['eventType'], $data);
}
}
}
}
9 changes: 9 additions & 0 deletions docs/xero-webhook-signing-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Xero webhook signing key

The admin Xero webhook endpoint validates Xero's `x-xero-signature` header with an HMAC over the raw request body.

Set the signing key at runtime:

- `XERO_WEBHOOK_SIGNING_KEY`

Do not commit the signing key to controller code or environment templates. If this variable is missing, webhook requests fail closed with HTTP 401 and a server-side configuration log entry.
22 changes: 22 additions & 0 deletions tests/check-xero-webhook-signature-hardening.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail

controller="admin/modules/v1/controllers/XeroWebhookController.php"
legacy_key_pattern="E4OxefKZm8uPKkiz8""RkGA8t"

if rg -n "$legacy_key_pattern" "$controller" docs --glob '!tests/check-xero-webhook-signature-hardening.sh'; then
echo "Xero webhook signing key must not be committed." >&2
exit 1
fi

rg -n "getenv\\('XERO_WEBHOOK_SIGNING_KEY'\\)" "$controller" >/dev/null
rg -n '\$request_data = Yii::\$app->request->rawBody;' "$controller" >/dev/null
rg -n 'hash_equals\(\$generated_signature, \$provided_signature\)' "$controller" >/dev/null
rg -n "UnauthorizedHttpException" "$controller" >/dev/null

if rg -n "echo \"Signature mismatch\"|die\\(\\)|http_response_code\\(401\\)" "$controller"; then
echo "Xero webhook signature failures should use framework 401 exceptions, not echo/die." >&2
exit 1
fi

echo "Xero webhook signature validation is runtime-configured and fail-closed."