Skip to content

Commit 1a589d1

Browse files
1 parent 54c4016 commit 1a589d1

5 files changed

Lines changed: 291 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-725g-w329-g7qr",
4+
"modified": "2026-03-12T14:50:43Z",
5+
"published": "2026-03-12T14:50:43Z",
6+
"aliases": [],
7+
"summary": "kora-lib: Token-2022 Transfer Fee Not Deducted During Payment Verification",
8+
"details": "## Summary\n\nWhen a user pays transaction fees using a Token-2022 token with a `TransferFeeConfig` extension, Kora's `verify_token_payment()` credits the full raw transfer `amount` as the payment value. However, the on-chain SPL Token-2022 program withholds a portion of that amount as a transfer fee, so the paymaster's destination account only receives `amount - transfer_fee`. This means the paymaster consistently credits more value than it actually receives, resulting in systematic financial loss.\n\n## Severity\n\n**High**\n\n## Affected Component\n\n- **File:** `crates/lib/src/token/token.rs`\n- **Function:** `verify_token_payment()`\n- **Lines:** 529–654 (specifically 633–639)\n\n## Root Cause\n\nIn `verify_token_payment()`, the `amount` extracted from the parsed SPL transfer instruction is the **pre-fee** amount (what the sender specifies in the `transfer_checked` instruction). The function passes this raw amount to `calculate_token_value_in_lamports()` to determine how many lamports the payment is worth. It never subtracts the Token-2022 transfer fee.\n\nThe fee estimation path (`fee.rs:analyze_payment_instructions`) correctly accounts for transfer fees by calculating them and adding them to the total fee. But the verification path does not perform the inverse subtraction, creating an asymmetry.\n\n## Vulnerable Code\n\n```rust\n// crates/lib/src/token/token.rs:529-654\npub async fn verify_token_payment(\n transaction_resolved: &mut VersionedTransactionResolved,\n rpc_client: &RpcClient,\n required_lamports: u64,\n expected_destination_owner: &Pubkey,\n) -> Result<bool, KoraError> {\n let config = get_config()?;\n let mut total_lamport_value = 0u64;\n\n // ...\n\n for instruction in transaction_resolved\n .get_or_parse_spl_instructions()?\n .get(&ParsedSPLInstructionType::SplTokenTransfer)\n .unwrap_or(&vec![])\n {\n if let ParsedSPLInstructionData::SplTokenTransfer {\n source_address,\n destination_address,\n mint,\n amount, // <-- This is the PRE-FEE amount from the instruction\n is_2022,\n ..\n } = instruction\n {\n // ... destination validation ...\n\n // LINE 633-639: Uses raw *amount without deducting transfer fee\n let lamport_value = TokenUtil::calculate_token_value_in_lamports(\n *amount, // <-- BUG: Should be (amount - transfer_fee)\n &token_mint,\n config.validation.price_source.clone(),\n rpc_client,\n )\n .await?;\n\n total_lamport_value = total_lamport_value\n .checked_add(lamport_value)\n .ok_or_else(|| {\n KoraError::ValidationError(\"Payment accumulation overflow\".to_string())\n })?;\n }\n }\n\n Ok(total_lamport_value >= required_lamports)\n}\n```\n\nFor comparison, the transfer fee calculation exists elsewhere in the codebase and is used during fee estimation:\n\n```rust\n// crates/lib/src/token/spl_token_2022.rs:165-198\npub fn calculate_transfer_fee(\n &self,\n amount: u64,\n current_epoch: u64,\n) -> Result<Option<u64>, KoraError> {\n if let Some(fee_config) = self.get_transfer_fee() {\n let transfer_fee = if current_epoch >= u64::from(fee_config.newer_transfer_fee.epoch) {\n &fee_config.newer_transfer_fee\n } else {\n &fee_config.older_transfer_fee\n };\n let basis_points = u16::from(transfer_fee.transfer_fee_basis_points);\n let maximum_fee = u64::from(transfer_fee.maximum_fee);\n let fee_amount = (amount as u128)\n .checked_mul(basis_points as u128)\n .and_then(|product| product.checked_div(10_000))\n // ...\n Ok(Some(std::cmp::min(fee_amount, maximum_fee)))\n } else {\n Ok(None)\n }\n}\n```\n\nThis function exists but is **never called** in `verify_token_payment()`.\n\n## Proof of Concept\n\n### Arithmetic Demonstration\n\nGiven:\n- Token-2022 token with 5% transfer fee (500 basis points), whitelisted in `allowed_spl_paid_tokens`\n- Transaction fee cost: 5000 lamports equivalent\n- Token price: 1 token = 5 lamports\n\n**What should happen:**\n- User needs to pay 5000 lamports worth → 1000 tokens\n- Transfer fee on 1000 tokens at 5% = 50 tokens\n- Paymaster destination receives: 1000 - 50 = 950 tokens (worth 4750 lamports)\n- User should be required to pay MORE to cover the fee\n\n**What actually happens:**\n- User sends `transfer_checked` for `amount = 1000` tokens\n- `verify_token_payment()` calculates: 1000 tokens * 5 lamports/token = 5000 lamports\n- 5000 >= 5000 required → **payment verified as sufficient**\n- But paymaster only received 950 tokens (worth 4750 lamports)\n- **Paymaster lost 250 lamports on this transaction**\n\n**Over 1000 transactions:** Paymaster loses 250,000 lamports (0.25 SOL)\n\n### Runnable Test (using existing test infrastructure)\n\n```rust\n#[tokio::test]\nasync fn test_token2022_transfer_fee_not_deducted_in_verification() {\n // Setup: Token-2022 mint with 10% transfer fee (1000 bps)\n let transfer_fee_config = create_transfer_fee_config(\n 1000, // 10% basis points\n u64::MAX, // no maximum fee cap\n );\n\n let mint_pubkey = Pubkey::new_unique();\n let mint_account = MintAccountMockBuilder::new()\n .with_decimals(6)\n .with_supply(1_000_000_000_000)\n .with_extension(ExtensionType::TransferFeeConfig)\n .build_token2022();\n\n // User sends transfer_checked for 1,000,000 tokens (1 token at 6 decimals)\n let transfer_amount: u64 = 1_000_000;\n\n // What verify_token_payment credits:\n let credited_amount = transfer_amount; // = 1,000,000\n\n // What the paymaster actually receives (after 10% on-chain fee):\n let actual_received = transfer_amount - (transfer_amount * 1000 / 10000); // = 900,000\n\n // BUG: credited_amount (1,000,000) > actual_received (900,000)\n // Paymaster is credited 11.1% MORE than it actually receives\n assert!(credited_amount > actual_received);\n assert_eq!(credited_amount - actual_received, 100_000); // 100,000 token units lost\n\n // The financial loss per transaction = 10% of the payment amount\n // This is NOT a rounding error — it is a full percentage-based loss\n}\n```\n\n## Impact\n\n- **Systematic Financial Loss:** The paymaster consistently credits more token value than it receives for every transaction paid with a transfer-fee-bearing Token-2022 token.\n- **Loss Scale:** Proportional to `transfer_fee_basis_points / 10000 * payment_amount` per transaction. For a token with 5% fee and 100 transactions/day at $1 each, that is $5/day or $1,825/year in losses.\n- **Precondition:** Requires a Token-2022 token with `TransferFeeConfig` extension to be whitelisted in `allowed_spl_paid_tokens`. The existing test infrastructure already creates such tokens (`TestAccountSetup::create_usdc_mint_2022()` with 100 bps / 1% fee).\n\n## Recommendation\n\nDeduct the Token-2022 transfer fee before calculating the lamport value of the payment:\n\n```rust\n// In verify_token_payment(), after extracting amount:\nlet effective_amount = if *is_2022 {\n // Fetch the mint to check for TransferFeeConfig\n let mint_account = CacheUtil::get_account(\n rpc_client,\n &token_mint,\n false,\n ).await?;\n let mint_info = Token2022MintInfo::from_account_data(&mint_account.data)?;\n\n if let Ok(Some(fee)) = mint_info.calculate_transfer_fee(\n *amount,\n rpc_client.get_epoch_info().await?.epoch,\n ) {\n amount.saturating_sub(fee)\n } else {\n *amount\n }\n} else {\n *amount\n};\n\nlet lamport_value = TokenUtil::calculate_token_value_in_lamports(\n effective_amount, // Use post-fee amount\n &token_mint,\n config.validation.price_source.clone(),\n rpc_client,\n)\n.await?;\n```\n\n## References\n\n- `crates/lib/src/token/token.rs:529-654` — `verify_token_payment()` using raw amount\n- `crates/lib/src/token/spl_token_2022.rs:165-198` — `calculate_transfer_fee()` (exists but not called in verification)\n- `crates/lib/src/fee/fee.rs:174-204` — `analyze_payment_instructions()` (correctly accounts for transfer fee in estimation)\n- SPL Token-2022 specification: transfer fees are deducted from the transfer amount by the on-chain program",
9+
"severity": [],
10+
"affected": [
11+
{
12+
"package": {
13+
"ecosystem": "crates.io",
14+
"name": "kora-lib"
15+
},
16+
"ranges": [
17+
{
18+
"type": "ECOSYSTEM",
19+
"events": [
20+
{
21+
"introduced": "0"
22+
},
23+
{
24+
"fixed": "2.0.5"
25+
}
26+
]
27+
}
28+
]
29+
}
30+
],
31+
"references": [
32+
{
33+
"type": "WEB",
34+
"url": "https://github.com/solana-foundation/kora/security/advisories/GHSA-725g-w329-g7qr"
35+
},
36+
{
37+
"type": "WEB",
38+
"url": "https://github.com/solana-foundation/kora/commit/8cbd8217ee505e6b37c63ef835ff095cfa8ab318"
39+
},
40+
{
41+
"type": "PACKAGE",
42+
"url": "https://github.com/solana-foundation/kora"
43+
}
44+
],
45+
"database_specific": {
46+
"cwe_ids": [],
47+
"severity": "MODERATE",
48+
"github_reviewed": true,
49+
"github_reviewed_at": "2026-03-12T14:50:43Z",
50+
"nvd_published_at": null
51+
}
52+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-8wq8-6859-qx77",
4+
"modified": "2026-03-12T14:51:06Z",
5+
"published": "2026-03-12T14:51:06Z",
6+
"aliases": [
7+
"CVE-2026-32237"
8+
],
9+
"summary": "@backstage/plugin-scaffolder-backend: Possible exposure of defaultEnvironment secrets using dry-run endpoint",
10+
"details": "### Impact \n \n Authenticated users with permission to execute scaffolder dry-runs can gain access to server-configured environment secrets through the dry-run API response. Secrets are properly \n redacted in log output but not in all parts of the response payload.\n \n Deployments that have configured `scaffolder.defaultEnvironment.secrets` are affected.\n \n ### Patches \n\n This is patched in `@backstage/plugin-scaffolder-backend` version 3.1.5\n ### Workarounds\n\n Remove or empty the `scaffolder.defaultEnvironment.secrets` configuration from `app-config.yaml`. Alternatively, restrict access to the scaffolder dry-run functionality via the\n permissions framework.\n\n ### References\n\n - [Backstage Scaffolder Backend documentation](https://backstage.io/docs/features/software-templates/)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "@backstage/plugin-scaffolder-backend"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "3.1.0"
29+
},
30+
{
31+
"fixed": "3.1.5"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/backstage/backstage/security/advisories/GHSA-8wq8-6859-qx77"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/backstage/backstage/commit/3b62dd2d6bf7623ebd23e4b5a6dceb209f98dfce"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/backstage/backstage"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-200"
55+
],
56+
"severity": "MODERATE",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-03-12T14:51:06Z",
59+
"nvd_published_at": null
60+
}
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-qp4c-xg64-7c6x",
4+
"modified": "2026-03-12T14:51:02Z",
5+
"published": "2026-03-12T14:51:02Z",
6+
"aliases": [
7+
"CVE-2026-32236"
8+
],
9+
"summary": "@backstage/plugin-auth-backend: SSRF in experimental CIMD metadata fetch",
10+
"details": "### Impact \n \n A Server-Side Request Forgery (SSRF) vulnerability exists in `@backstage/plugin-auth-backend` when `auth.experimentalClientIdMetadataDocuments.enabled` is set to `true`. The CIMD \n metadata fetch validates the initial `client_id` hostname against private IP ranges but does not apply the same validation after HTTP redirects.\n \n The practical impact is limited. The attacker cannot read the response body from the internal request, cannot control request headers or method, and the feature must be explicitly\n enabled via an experimental flag that is off by default. Deployments that restrict `allowedClientIdPatterns` to specific trusted domains are not affected.\n\n ### Patches\n\n Patched in `@backstage/plugin-auth-backend` version `0.27.1`. The fix disables HTTP redirect following when fetching CIMD metadata documents.\n\n ### Workarounds\n\n Disable the experimental CIMD feature by removing or setting `auth.experimentalClientIdMetadataDocuments.enabled` to `false` in your app-config. This is the default configuration.\n Alternatively, restrict `allowedClientIdPatterns` to specific trusted domains rather than using the default wildcard pattern.\n\n ### References\n\n - [IETF Client ID Metadata Document draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)\n - [MCP Authorization Specification - Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "@backstage/plugin-auth-backend"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.27.1"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/backstage/backstage/security/advisories/GHSA-qp4c-xg64-7c6x"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/backstage/backstage/commit/17038abf2dfdb4abc08a59b1c95af39851de0e07"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/backstage/backstage"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-918"
55+
],
56+
"severity": "LOW",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-03-12T14:51:02Z",
59+
"nvd_published_at": null
60+
}
61+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-wqvh-63mv-9w92",
4+
"modified": "2026-03-12T14:50:59Z",
5+
"published": "2026-03-12T14:50:59Z",
6+
"aliases": [
7+
"CVE-2026-32235"
8+
],
9+
"summary": "@backstage/plugin-auth-backend: OAuth redirect URI allowlist bypass",
10+
"details": "### Impact\n\nThe experimental OIDC provider in `@backstage/plugin-auth-backend` is vulnerable to a redirect URI allowlist bypass. Instances that have enabled experimental Dynamic Client Registration or Client ID Metadata Documents and configured `allowedRedirectUriPatterns` are affected.\n\nA specially crafted redirect URI can pass the allowlist validation while resolving to an attacker-controlled host. If a victim approves the resulting OAuth consent request, their authorization code is sent to the attacker, who can exchange it for a valid access token.\n\nThis requires victim interaction and that one of the experimental features is explicitly enabled, which is not the default.\n\n### Patches\n\nUpgrade to `@backstage/plugin-auth-backend` version 0.27.1 or later.\n\n### Workarounds\n\nDisable experimental Dynamic Client Registration and Client ID Metadata Documents features if they are not required.\n\n### References\n\n- [RFC 6749 Section 3.1.2 - Redirection Endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2)",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "@backstage/plugin-auth-backend"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "0.27.1"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/backstage/backstage/security/advisories/GHSA-wqvh-63mv-9w92"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/backstage/backstage/commit/6042dd0c7f0706e0f473dafa92799ecf19c825ec"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/backstage/backstage"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-20",
55+
"CWE-601"
56+
],
57+
"severity": "MODERATE",
58+
"github_reviewed": true,
59+
"github_reviewed_at": "2026-03-12T14:50:59Z",
60+
"nvd_published_at": null
61+
}
62+
}

0 commit comments

Comments
 (0)