commit 9a5461eff67ed94c1e9beab060d38bc52e6652f6
parent 44988c85c2fe24478bdc5b48f13dba18137f4f47
Author: Florian Dold <dold@taler.net>
Date: Thu, 24 Sep 2026 00:25:02 +0200
harness: check that a forged challenger error redirect cannot fail KYC
test-kyc-challenger now uses the state the exchange issued, as the exchange
rejects the bare account hash.
Issue: https://bugs.taler.net/n/11740
Diffstat:
2 files changed, 43 insertions(+), 19 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-challenger.ts b/packages/taler-harness/src/integrationtests/test-kyc-challenger.ts
@@ -346,12 +346,17 @@ export async function runKycChallengerTest(t: GlobalTestState) {
);
logger.info(`kyc-start resp status: ${startResp.status}`);
- logger.info(j2s(await startResp.json()));
+ const startJson = await startResp.json();
+ logger.info(j2s(startJson));
+ // The provider hands back the state the exchange put into the
+ // authorization URL; the exchange rejects any other.
+ const state = new URL(startJson.redirect_url).searchParams.get("state");
+ t.assertTrue(!!state);
// We need to "visit" the KYC proof URL at least once to trigger the exchange
// asking for the KYC status.
const proofUrl = new URL(`kyc-proof/myprov`, exchange.baseUrl);
- proofUrl.searchParams.set("state", paytoHash);
+ proofUrl.searchParams.set("state", state);
proofUrl.searchParams.set("code", "code_is_ok");
// Check the exchange's completion redirect without visiting an external
// website, whose availability is unrelated to this local KYC flow.
diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-challenger-failure.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-challenger-failure.ts
@@ -49,6 +49,10 @@ import {
* (bug 11740). The exchange must then finish the KYC process as failed
* instead of treating the answer as an unexpected provider reply and
* leaving the process pending.
+ *
+ * As such a redirect is not authenticated, the exchange must only trust
+ * it if it carries the unguessable state the exchange issued for the
+ * process; anyone knowing just the account must not be able to fail it.
*/
export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) {
const {
@@ -153,6 +157,37 @@ export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) {
const errorRedirectUrl = authorizeJson.error_redirect_url;
t.assertTrue(typeof errorRedirectUrl === "string");
+ const hPayto = `decode('${Buffer.from(decodeCrock(merchantPaytoHash)).toString("hex")}', 'hex')`;
+ const getProcessState = async () => {
+ const processState = await runCommand(t, "kyc-process-state", "psql", [
+ commonDb.connStr,
+ "-X",
+ "-qAt",
+ "-v",
+ "ON_ERROR_STOP=1",
+ "-c",
+ `SELECT finished, error_code
+ FROM exchange.legitimization_processes
+ WHERE h_payto=${hPayto}
+ AND provider_name='sms-challenger'
+ ORDER BY legitimization_process_serial_id DESC
+ LIMIT 1`,
+ ]);
+ console.log(`KYC process state: ${processState}`);
+ return processState.trim();
+ };
+
+ // Someone who only knows the account forges the error redirect.
+ const forgedUrl = new URL(errorRedirectUrl);
+ t.assertTrue(forgedUrl.searchParams.get("state") !== merchantPaytoHash);
+ forgedUrl.searchParams.set("state", merchantPaytoHash);
+ const forgedResp = await harnessHttpLib.fetch(forgedUrl.href, {
+ redirect: "manual",
+ });
+ console.log("forged proof status:", forgedResp.status);
+ t.assertDeepEqual(forgedResp.status, 403);
+ t.assertDeepEqual((await getProcessState()).split("|")[0], "f");
+
// The user used up all their attempts; challenger sends them back.
const proofResp = await harnessHttpLib.fetch(errorRedirectUrl, {
redirect: "manual",
@@ -162,24 +197,8 @@ export async function runTopsAmlChallengerFailureTest(t: GlobalTestState) {
// unexpected provider reply.
t.assertDeepEqual(proofResp.status, 403);
- const hPayto = `decode('${Buffer.from(decodeCrock(merchantPaytoHash)).toString("hex")}', 'hex')`;
- const processState = await runCommand(t, "kyc-process-state", "psql", [
- commonDb.connStr,
- "-X",
- "-qAt",
- "-v",
- "ON_ERROR_STOP=1",
- "-c",
- `SELECT finished, error_code
- FROM exchange.legitimization_processes
- WHERE h_payto=${hPayto}
- AND provider_name='sms-challenger'
- ORDER BY legitimization_process_serial_id DESC
- LIMIT 1`,
- ]);
- console.log(`KYC process state: ${processState}`);
t.assertDeepEqual(
- processState.trim(),
+ await getProcessState(),
`t|${TalerErrorCode.EXCHANGE_GENERIC_KYC_FAILED}`,
);
}