Skip to content

chore(release): merge release/4.7.1 to main - #2000

Merged
amandagonsalves merged 3 commits into
mainfrom
chore/merge-release-4.7.1-to-main
Aug 26, 2026
Merged

amandagonsalves merged 3 commits into
mainfrom
chore/merge-release-4.7.1-to-main

Conversation

@amandagonsalves

Copy link
Copy Markdown
Collaborator

Description

Merges release/4.7.1 into main for the 4.7.1 release.

Context

N/A

Testing

./gradlew test

Documentation

N/A

Known limitations

N/A

ceciliaromao and others added 3 commits August 20, 2026 14:15
Merges main into develop for the 4.7.0 release.
### Description

[ANCHOR-1224](#1965) (fix
for [HackerOne #3810399](https://hackerone.com/reports/3810399)) added a
mandatory `aud` claim check to `JwtService.decode(...,
PlatformAuthJwt.class)`: any token verified against `platformAuthSecret`
must now carry `aud=platform_api`, or decoding throws. That PR updated
every place inside the Java `core`/`platform` modules that mints or
verifies this token type, but missed the one place outside them that
also mints it: the Kotlin reference server's
`JwtTokenProvider.createJwt`, used by `SepHelper` (via
`AuthHeaderUtil.addAuthHeaderIfNeeded`) to sign the reference server's
own outgoing calls to the platform's Platform API (`GET
/transactions/{id}`, `PATCH /transactions`, and the JSON-RPC endpoint).

`createJwt` only ever set `issuedAt`/`expiration` — no `aud` claim at
all. So in any deployment where an operator turns on
`platform_api.auth.type=jwt` — which they should, since that's what
actually closes the vulnerability ANCHOR-1224 patched — every one of the
reference server's calls into the platform's Platform API is
unconditionally rejected: `JwtService.decode` sees a null audience,
throws, and `PlatformAuthJwtFilter`/`AbstractJwtFilter` returns a
generic `403 {"error":"forbidden"}`.

Two separate logging gaps in the existing code compounded to make this
extremely hard to trace back to the actual cause, rather than something
wrong with the shared secret itself:

- `AbstractJwtFilter.doFilter` catches the exception from `check(...)`
and discards it entirely before calling `sendForbiddenError`, so the
platform's own logs never show *why* a request was rejected — audience
mismatch and a wrong/rotated secret look identical from the outside.
- `SepHelper.getTransaction()` never checked the HTTP response status
before deserializing the body as a `Transaction`, so the `403`'s
`{"error":"forbidden"}` body got fed straight into `Transaction`'s
deserializer, producing an unrelated-looking `Illegal input: Fields [id,
status, kind] are required...` error instead. That's the error that
actually surfaces in the SEP-24 Reference UI's backend and is what made
this look like a JWT-secret-mismatch issue on first inspection — it
isn't; the shared secret is fine, the token is simply missing a claim
the verifier now requires.

The end-user symptom: the SEP-24 Reference UI (what wallets are sent to
for the interactive deposit/withdraw flow) shows an empty transaction
with no form and no action button — the UI's backend can't fetch the
transaction record it needs to render, but the underlying cause never
made it into any visible error message.

**Changes**
- [x] `JwtTokenProvider.createJwt` (`kotlin-reference-server`): adds
`.audience().add(AUD_PLATFORM_API).and()` before signing, where
`AUD_PLATFORM_API = "platform_api"` matches
`JwtService.AUD_PLATFORM_API` on the platform side. Only call site is
`AuthHeaderUtil.addAuthHeaderIfNeeded`, itself only used for the
reference server's calls to the platform's Platform API, so the audience
is hardcoded rather than threaded through as a parameter.

**Acceptance Criteria**
- [x] With `platform_api.auth.type=jwt` set and the reference server's
`anchorToPlatformSecret` equal to the platform's
`secret.platform_api.auth_secret`, `SepHelper.getTransaction()` (and
`patchTransaction`/`rpcAction`) succeed against the platform's Platform
API.
- [x] A token minted by `JwtTokenProvider.createJwt` before this fix (no
`aud` claim) is rejected by `JwtService.decode(...,
PlatformAuthJwt.class)` — confirms the regression this fix closes.
- [x] The SEP-24 interactive flow, driven end-to-end (SEP-10 → SEP-24
initiate → open interactive URL → reference server's `/start` then
`/transaction`), returns the real transaction record instead of
`{"error":"forbidden"}` or a deserialization error.

### Context

Regression from
[ANCHOR-1224](#1965) (fix
for [HackerOne #3810399](https://hackerone.com/reports/3810399)).

### Testing

- Manual: with `TEST_PROFILE_NAME=auth-jwt-platform` and
`RUN_SEP_SERVER=true` (`./gradlew dockerComposeStart && ./gradlew
startServersWithTestProfile`), run a SEP-24 withdraw, then reproduce the
reference server's own next two calls directly:
  ```bash
curl -X POST http://localhost:8091/start -H "Authorization: Bearer
<interactive-url-token>"
curl http://localhost:8091/transaction -H "Authorization: Bearer
<sessionId-from-/start>"
  ```
Before this fix: `/transaction` returns `{"msg":"Error occurred:
{\"error\": \"forbidden\"}"}`. After: it returns the full transaction
JSON (`id`, `status`, `kind`, `amount_expected`, etc.).

### Documentation
N/A

### Known limitations
N/A

[ANCHOR-1224]:
https://stellarorg.atlassian.net/browse/ANCHOR-1224?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[ANCHOR-1224]:
https://stellarorg.atlassian.net/browse/ANCHOR-1224?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
### Description

This bumps the version to 4.7.1

### Context

Release

### Testing

`./gradlew test`

### Documentation

N/A

### Known limitations

N/A
@amandagonsalves
amandagonsalves merged commit 3ebdffa into main Aug 26, 2026
17 checks passed
@amandagonsalves
amandagonsalves deleted the chore/merge-release-4.7.1-to-main branch August 26, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants