Back to Resources

I Checked My Golf Club's App Before Saving My Credit Card

Two foreUP API flaws, CVE-2026-15657 and CVE-2026-15658, exposed a golf club's payment processor credentials and member records. Full disclosure timeline.

I Checked My Golf Club's App Before Saving My Credit Card

Last summer I joined a golf club. The club runs its tee sheet, its dining minimum, and its monthly dues through foreUP, a golf course management platform. Setting up the account meant putting a credit card on file.


CVE-2026-15657, CVE-2026-15658, CERT/CC VU#790363


So I did what I do for a living. Before I typed a card number into an application I’d never seen, I put the browser behind an intercepting proxy and watched what the app actually did. It took about ten minutes to find something I didn’t expect in the API responses, and another ten to find something worse.


Both are public now. CERT/CC published VU#790363 on July 30, 2026, and the vendor confirmed remediation on July 26. Eleven months passed between my first email and that confirmation.


The First Finding: The Facility’s Payment Credentials Were in My Own Profile

The web app loads your customer record from a REST endpoint shaped like this:

GET /api_rest/index.php/courses/{cid}/oco/golfer/{golfer_id} HTTP/1.1
X-Authorization: Bearer <JWT>


That returns your profile as JSON. Name, email, phone, date of birth, address, account balance, stored payment methods. Verbose, but normal.


Inside each stored card object were three fields I had no business seeing:

"credit_cards": [
  {
    "credit_card_id": "…",
    "card_type": "VISA",
    "masked_account": "<CARD_LAST4>",
    "finix_username":    "<FINIX_USERNAME>",
    "finix_password":    "<FINIX_PASSWORD>",
    "finix_merchant_id": "<FINIX_MERCHANT_ID>",
    "token": "<CARD_TOKEN>"
  }
]


Finix is a payment processor. Those three fields are a merchant API credential pair: a username, a UUID serving as the API key, and the merchant identifier that scopes the account. They were coming back in cleartext to my browser, in response to a request I made about myself.


Two follow up observations made this worse than a stray debug field.


They weren’t scoped to me. I added a second card and re-read my record. Identical values. I also have access to a second household account, used with consent, and its card object carried the same three values, byte for byte. The credentials belong to the facility, not to a user and not to a card. One member reading their own profile gets the whole club’s merchant credentials.


They were live. I didn’t need to test them against Finix to establish that. When I added the second card, the number went to a tokenization vault, and the browser handed the resulting token back to foreUP to register against my account. When I re-fetched my record, the stored token wasn’t the vault’s identifier. It was a fresh Finix payment instrument id. A Finix payment instrument only exists if something authenticated to Finix with valid merchant credentials and got a 201 back. The backend did that, minutes earlier, using the credentials the client could read.


What that gets an attacker. Finix authenticates API callers with HTTP Basic auth, where the username is a User resource id and the password is that user’s secret key. That is exactly the pair sitting in the JSON, which means anyone holding it is the facility as far as Finix is concerned. The API is organized around Transfers, any flow of funds to or from a payment instrument, so the reachable operations include creating a debit against a card, reversing a transfer to issue a refund, and listing the facility’s payment history. Charging a card also requires a payment instrument id to charge, and the profile endpoint hands those out too: mine in my own record, and, as the next section shows, every other member’s in theirs. Merchant credentials plus another member’s stored card token is the entire input set for a charge against a card that isn’t yours, billed through the club’s own merchant account.


To be clear about what I did and didn’t do: I never sent a request to Finix with those credentials. Establishing that they were live took an inference from foreUP’s own behavior, not a test against a payment API, and measuring the blast radius any further would have meant moving money that wasn’t mine. The capability above is what credentials at that level are designed to permit, read off Finix’s public API documentation.


That’s CVE-2026-15657.


The Second Finding: Broken Object Level Authorization

The golfer_id in that URL is a plain integer. So I changed it.


Same session. Same JWT. Same everything except the number in the path. What came back was the other account’s complete record: name, email, both phone numbers, date of birth, home address, free text household notes, a tokenized ACH funding source with its verification state and prior charge count, a Finix card token, and seven months of transaction history.


The authorization material was already in the request. The JWT was HS512 signed, so I couldn’t forge one, and authentication was genuinely enforced. The payload looked like this:

{
  "iss": "foreupsoftware.com",
  "uid": "<GolferA>",
  "cid": "{cid}",
  "level": 0,
  "employee": false
}


The uid claim is the server’s own signed statement of who I am. It sat in every request, next to a path segment naming a different customer, and nothing compared the two.


That’s CVE-2026-15658.


Ruling Out the Boring Explanation

foreUP’s data model has households. Transaction rows carry a trans_household field, and profile comments name spouses. So “you have legitimate household linked read access” is a real alternative explanation, and my two accounts were in fact a household. I needed to eliminate it without touching a stranger’s data.


Two probes did it:

Request Response
golfer/99999999 (no such customer) 200 OK, record shaped body, mostly blank
golfer/0 (system reserved) 200 OK, populated system record


A household scoped endpoint rejects before it looks anything up. This one looked things up and returned whatever it found, for valid ids, other people’s ids, nonexistent ids, and reserved ids alike. There was no ownership check to scope. Two requests closed the question, with zero exposure to anyone outside my own household.


API Security Lessons

Don’t serialize your database row to the client. Both CVEs come out of one antipattern. The customer record object was returned whole, so it carried customer data and backend processor secrets in the same payload. The fix is a response DTO with an explicit field allowlist. A blocklist of a few sensitive names missed finix_password here, and it will miss whatever the next payment integration adds. This is exactly the class of defect a secure code review is built to catch before it ships.


The authorization data was already in the request. BOLA survives not because owner identity is hard to obtain, but because the handler never asks. Derive the owner from the token or session, compare it to the requested object, and make the rejection identical whether the object belongs to someone else or doesn’t exist at all. The status code should never be an existence oracle.


Tenant wide credentials turn one leak into a tenant compromise. Facility scoped merchant credentials meant any single member equaled the facility’s payment account. Scope secrets as narrowly as the design allows, and keep them off the client regardless of scope.


Removing a field is not rotating a secret. Closing the leak channel stops new disclosure. It does nothing about credentials already harvested during the exposure window. Anyone who read that endpoint over eleven months still holds a working credential pair until Finix issues a new one. Rotation is the remediation. Field removal is the prerequisite, because rotating without fixing the serialization just publishes the new credentials on the next read.


The Disclosure Timeline

The technical work was the easy part.


I sent a good faith notice to vendor support on August 6, 2025, with no vulnerability details, asking only for a secure channel. No reply. I tried security@ at the vendor in the same window and it bounced. No security.txt was published at any vendor or parent company domain. I followed up on August 13. No reply.


Then eight months of nothing. I didn’t escalate, didn’t retest, didn’t tell anyone.


On March 30, 2026, the vendor’s parent merged into a larger company, which made intake harder rather than easier. On April 19 I ran a fresh, scope limited reproduction: my own account, the consented household account, and the two non customer probes. Read only. On April 20 I sent a second disclosure across every reachable channel, including the new parent’s privacy addresses, and started a 30 day clock. Back came one automated ticket acknowledgement and one bounce, from the address published as the vendor’s own EEA representative under GDPR Article 27.


On May 20 the deadline passed with no human response, and I submitted to CERT/CC. They drafted the note for July 15 with two CVEs assigned. The vendor surfaced at the last hour, after eleven months of silence toward both me and the coordinator. CERT/CC held publication for a remediation window rather than releasing on the draft date.


Once they engaged, they engaged properly. On July 21 they asked in writing, through the coordinator’s portal, for video of the vulnerability. I recorded two demonstrations of the read side bypass and delivered them the same way. Those runs used my own account and one golfer id I typed at random, which resolved to a real customer. Their request named no scenario. That choice was mine. They acknowledged the same day and escalated to their development team. Then they shipped the fix, confirmed it on July 26, and CERT/CC published on July 30.


I checked the fix myself rather than take it on faith. Same two consented accounts, same request, one changed integer in the path. It returns 403 now, and the finix_* fields are gone from the response body entirely. A nonexistent id returns 403 as well, so the rejection is uniform and the status code isn’t an existence oracle. That’s the behavior you want. Whether the exposed merchant credentials were rotated at Finix or just hidden from the API isn’t something I can determine from outside, and I’m not going to find out by exercising an old credential pair. That question belongs to foreUP and Finix.


Credit where it’s due: they fixed it. That matters more than the eleven months, and the eleven months is why a coordinator existed in this story at all.


The durable fix on the researcher side is a published disclosure policy with a hard deadline, cited in the first email. Eight months of silence cost this vendor nothing, because nothing was scheduled to happen at the end of them. A deadline stated up front changes what silence means. It stops being a way to wait me out and becomes a decision with a date attached.


About The Author

Eric Mead is a Principal Security Engineer at Puma Security. Want an application or API assessment that finds this class of flaw before your customers do? Learn more about our security services, browse our security research and resources, or contact us today: sales [at] pumasecurity [dot] io.


CERT/CC Vulnerability Note: VU#790363. Published July 30, 2026.


Discovery and reproduction used accounts I own or had explicit consent to use, plus two probe ids with no customer behind them. The one exception is the July 2026 video demonstrations, which the vendor asked for during remediation and which included one live customer record; those went to the vendor through CERT/CC’s secure portal and nowhere else. Nothing was retained, and no payment instrument belonging to anyone else was ever exercised. All identifiers, credentials, and tokens in this post are placeholders.


Hero image generated with Nano Banana 2. It is an illustration, not a screenshot of the affected application.