Skip to content

fix(hmac): stop dropping notification fields whose value is the string "0" - #934

Open
jaideeppyne wants to merge 2 commits into
Adyen:mainfrom
jaideeppyne:fix/hmac-falsy-string-fields
Open

fix(hmac): stop dropping notification fields whose value is the string "0"#934
jaideeppyne wants to merge 2 commits into
Adyen:mainfrom
jaideeppyne:fix/hmac-falsy-string-fields

Conversation

@jaideeppyne

Copy link
Copy Markdown

Description

getNotificationDataToSign() defaults every field with empty(). In PHP empty("0") is true, so a signed field holding the string "0" goes into the signing string as "". Adyen computes its signature over the real value, so a legitimate webhook fails validation. The realistic one to hit is merchantReference of "0", but the same holds for pspReference, originalReference, merchantAccountCode, amount.currency, eventCode and success.

amount.value was already moved to isset for exactly this reason, and the comment on that line says so. I applied the same treatment to the remaining seven fields using ??.

The signed field list is the documented one, pspReference:originalReference:merchantAccountCode:merchantReference:value:currency:eventCode:success, and only the defaulting of those values changes here. Nothing about separators or escaping is touched.

Tested scenarios

The signing string is one contract with several implementations, so I used the sibling SDKs as the oracle. For each of the 6 cleanly comparable fields I built the same notification item and compared signatures from adyen-python, adyen-java, adyen-go and adyen-node. All four agree with each other on all 6 cases, and this library was the only one that disagreed, on all 6. After the fix it matches all four.

I also ran the 4 real Adyen-signed fixtures that ship in adyen-python-api-library (test/mocks/util/*_notification.json, the ones carrying colons and backslashes in merchantReference) through isValidNotificationHMAC here. All 4 still validate, so values Adyen signs today are unaffected.

New tests are testMerchantReferenceZeroIsNotTreatedAsEmpty and a 6 case data provider asserting that "0" and "" do not collapse to the same signature. Reverting only HmacSignature.php and keeping the new tests gives 7 failures; with the fix the unit suite is 295/295 green, up from 288.

Not covered: success only ever carries "true" or "false" from Adyen and the Java model types it as a bool, so I could not derive a cross SDK constant for it. It is defaulted consistently in the fix but has no pinned test vector.

I used Claude Code to build the cross SDK harness and draft the tests. I ran and checked every signature and test result myself.

Fixed issue: n/a

…g "0"

getNotificationDataToSign used empty() to default each field, so a field
holding the string "0" was signed as an empty string. A webhook with
merchantReference "0" therefore failed validation against Adyen's own
signature. The value field already used isset for this reason; apply the
same treatment to the remaining fields.
@jaideeppyne
jaideeppyne requested a review from a team as a code owner August 31, 2026 12:40

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the HMAC signature generation in HmacSignature.php to use the null coalescing operator (??) instead of empty() checks, preventing values like '0' from being incorrectly treated as empty. It also adds comprehensive unit tests to verify this behavior. The feedback suggests making the test helper method notificationWithField more robust by handling the nested 'value' field under 'amount' alongside 'currency'.

Comment thread tests/Unit/Util/HmacSignatureTest.php Outdated
@jaideeppyne

Copy link
Copy Markdown
Author

Good catch, done in the follow-up commit.

It's a bit worse than a future-proofing issue too. The old helper would have set value at the root and left amount.value at 1130, so the signature would still be computed over 1130 and the test would pass without testing anything.

I left value out of the provider because amount.value already uses isset and so never had this bug, but the helper should be right regardless.

@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant