Unauthenticated Team Income Export Exposes Donor Identities: How Frozen Visibility Bypasses Privacy Controls in Crowdfunding Platforms + Video

Listen to this Post

Featured Image

Introduction

Privacy-preserving donation platforms rely on granular user settings such as “hide my giving” and “hide me from public lists” to protect donor identities from public exposure. However, when export endpoints cache historical visibility states rather than evaluating current privacy preferences, unauthenticated attackers can reconstruct complete donation histories—including donor usernames, exact amounts, currencies, and timestamps—simply by requesting a JSON or CSV export. The Liberapay vulnerability disclosed via HackerOne (report ID not publicly listed, bounty $100) demonstrates that even mature bug bounty programs can ship endpoints that treat privacy as a one-time snapshot rather than a continuous enforcement boundary.

Learning Objectives

  • Understand how unauthenticated API endpoints can expose donor PII and financial relationships when privacy settings are not re-evaluated at export time.
  • Learn to audit payment-history and reporting endpoints for frozen visibility fields that bypass `hide_giving` and `hide_from_lists` controls.
  • Implement defensive coding patterns and runtime checks to ensure current privacy preferences always override historical payment records.

You Should Know

  1. The Anatomy of the Vulnerability: Frozen Visibility vs. Current Privacy

The core flaw lies in treating a donation’s visibility as a static attribute stored alongside the payment record. When a donor contributes publicly and later toggles their privacy settings to “private” or “secret,” the platform’s public-facing pages respect the updated preference—but the team income export endpoint continues to use the original visibility value frozen at the time of payment. This creates a persistent disclosure surface: any unauthenticated request to `/team/income/payments.json` or `.csv` returns the complete payment history, including donations that should have been retroactively hidden.

Why this matters for security engineers: Many applications cache computed fields for performance. Export endpoints are particularly prone to this anti-pattern because they are often built as batch reporting tools rather than real-time privacy checkpoints. The Liberapay endpoint did not query the current `donations.visibility` column; it relied on a denormalized `payment.visibility_snapshot` field that was never updated when donors changed their preferences.

Step-by-step guide to testing for this flaw:

  1. Map the export endpoint – Identify all unauthenticated or low-privilege export routes. Common patterns include /team/{team_id}/income/payments.json, /api/v1/teams/{team_id}/exports, and /reports/payments.csv?team=....
  2. Create a test donor – Register two accounts: one team admin and one donor. Make a public donation from the donor to the team.
  3. Enable privacy controls – On the donor account, enable “hide my giving” and “hide me from public lists.” Wait for any cache invalidation to propagate.
  4. Request the export unauthenticated – Use `curl` or a browser to fetch the export URL without cookies or tokens.
  5. Compare the results – If the donor’s name, amount, and date still appear in the export, the endpoint is using frozen visibility.

Linux/curl command to test:

 Attempt to fetch the payment export without authentication
curl -X GET "https://target-platform.com/team/liberapay/income/payments.json" \
-H "Accept: application/json" \
-H "User-Agent: Mozilla/5.0 (Security Test)" \
--verbose

Request CSV format (often less protected)
curl -X GET "https://target-platform.com/team/liberapay/income/payments.csv" \
-H "Accept: text/csv" \
--output payments_export.csv

Check if the export contains the test donor's identifier
grep -i "testdonor" payments_export.csv

Windows/PowerShell equivalent:

 Using Invoke-WebRequest for JSON export
Invoke-WebRequest -Uri "https://target-platform.com/team/liberapay/income/payments.json" -Method Get -Headers @{"Accept"="application/json"} -OutFile payments.json

Parse and search for donor username
Get-Content payments.json | Select-String "testdonor"

2. API Endpoint Auditing: Finding Hidden Export Routes

Export functionality often lurks in unexpected places: admin dashboards, team settings, reporting modules, and even undocumented GraphQL queries. Attackers scan for patterns like export, download, report, payments, income, and `history` combined with json, csv, xlsx, or pdf. The Liberapay endpoint was straightforward (/team/income/payments.json), but many platforms nest exports behind query parameters or different API versions.

Step-by-step guide for endpoint discovery:

  1. Review client-side JavaScript – Open browser DevTools, navigate to the Network tab, and trigger any “Export” or “Download” button. Capture the request URL, headers, and parameters.
  2. Check robots.txt and sitemap – Some platforms inadvertently list export endpoints in `robots.txt` or XML sitemaps.
  3. Fuzz common patterns – Use a wordlist of export-related terms against the domain’s API base path.
  4. Test for IDOR – If the endpoint requires a team ID, try changing it to another team’s ID. Many exports lack proper authorization checks beyond the ID parameter.
  5. Inspect GraphQL introspection – If GraphQL is enabled, query the schema for fields like payments, exports, or reports.

Burp Suite / OWASP ZAP configuration:

  • Set up a passive scan for any response containing `Content-Disposition: attachment` or filename=.csv.
  • Use the “Discover Content” feature to crawl for hidden directories.
  • Configure a custom wordlist with entries: income, payments, export, report, download, history, transactions, donations.

Linux reconnaissance command:

 Use ffuf to fuzz for export endpoints
ffuf -u https://target-platform.com/FUZZ/payments.json -w /usr/share/wordlists/dirb/common.txt -fc 404

Use gau (GetAllUrls) to find historical URLs from archives
gau target-platform.com | grep -E "(export|report|payments|income|download)"
  1. Exploitation Chain: From Unauthenticated Export to OSINT Goldmine

An unauthenticated export that leaks donor identities and amounts creates a powerful OSINT (Open Source Intelligence) vector. Attackers can correlate donation amounts with public social media profiles, infer political or ideological affiliations, and build detailed financial profiles of both donors and recipients. The “frozen visibility” aspect makes this especially dangerous because donors who explicitly opted out of public listing remain exposed indefinitely.

Step-by-step guide to demonstrating impact:

  1. Collect multiple exports – Repeatedly fetch the export over time to build a historical dataset. Even if the platform eventually fixes the flaw, cached responses or archived pages may persist.
  2. Cross-reference with public data – Match donor usernames against GitHub, Twitter, LinkedIn, or other platforms where the same handle is used.
  3. Aggregate by team – Identify which teams receive the most funding and from which donors. This reveals influence networks.
  4. Track changes – Compare exports before and after a donor changes privacy settings. If the donor disappears from the public page but remains in the export, the flaw is confirmed.

Python script for automated export monitoring:

import requests
import json
import time
from datetime import datetime

def fetch_payment_export(team_id, output_file):
url = f"https://target-platform.com/team/{team_id}/income/payments.json"
headers = {"Accept": "application/json", "User-Agent": "PrivacyAudit/1.0"}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
data = response.json()
with open(output_file, 'w') as f:
json.dump(data, f, indent=2)
print(f"[+] Export saved to {output_file} at {datetime.now()}")
return data
else:
print(f"[-] Failed: HTTP {response.status_code}")
return None
except Exception as e:
print(f"[!] Error: {e}")
return None

Example: monitor a team over time
team_id = "liberapay"
for i in range(5):
fetch_payment_export(team_id, f"export_snapshot_{i}.json")
time.sleep(3600)  Wait one hour between snapshots

4. Mitigation: Real-Time Privacy Validation in Export Endpoints

The fix for this vulnerability is conceptually simple: do not trust stored visibility flags. Instead, join the payment records with the current donation visibility table and apply the donor’s active privacy settings at query time. If a donation’s visibility has been updated, the export must reflect that change immediately.

Step-by-step guide for secure export implementation:

  1. Replace frozen columns with live joins – Instead of SELECT FROM payments WHERE team_id = ?, use SELECT p., d.current_visibility FROM payments p JOIN donations d ON p.donation_id = d.id WHERE p.team_id = ? AND d.current_visibility != 'private'.
  2. Apply donor-level filters – Add `AND (d.donor_privacy_settings & MASK_HIDE_GIVING) = 0` to exclude donors who have enabled “hide my giving.”
  3. Re-evaluate on every request – Never cache export results for more than a few seconds. If caching is necessary, invalidate the cache whenever a donation’s visibility changes.
  4. Authenticate export requests – Require at least a session cookie or API token. If the export must be public, ensure it only includes donations that are explicitly marked as public by both the donor and the team.
  5. Log all export accesses – Maintain an audit trail of who accessed which export and when. This helps detect unauthorized scraping.

Example SQL fix (PostgreSQL):

-- Before: using frozen visibility
CREATE VIEW team_income_export_old AS
SELECT p.donor_username, p.amount, p.currency, p.payment_date
FROM payments p
WHERE p.team_id = current_setting('app.current_team_id')::int;

-- After: respecting current privacy
CREATE VIEW team_income_export_new AS
SELECT p.donor_username, p.amount, p.currency, p.payment_date
FROM payments p
JOIN donations d ON p.donation_id = d.id
WHERE p.team_id = current_setting('app.current_team_id')::int
AND d.current_visibility IN ('public', 'team_only') -- not 'private' or 'secret'
AND (d.donor_privacy_settings & 1) = 0; -- bit 0 = hide_giving

Application-layer fix (Python/Django example):

from django.db import models

class PaymentExportView:
def get_queryset(self, team_id):
return Payment.objects.filter(
team_id=team_id,
donation__current_visibility__in=['public', 'team_only'],
donation__donor__privacy_settings__hide_giving=False
).select_related('donation__donor')

5. Cloud and Infrastructure Hardening for Export Endpoints

Beyond code-level fixes, infrastructure controls can limit the blast radius of misconfigured exports. Apply the principle of least privilege at the network, load balancer, and API gateway layers.

Step-by-step guide for cloud hardening:

  1. Restrict export endpoints to authenticated users – At the API gateway (AWS API Gateway, Cloudflare, or NGINX), require a valid session token or API key before routing to the export service.
  2. Implement rate limiting – Set aggressive rate limits on export endpoints to prevent bulk scraping. For example, allow only 5 exports per minute per IP.
  3. Use WAF rules – Deploy Web Application Firewall rules to block requests containing `payments.json` or `export.csv` from unauthenticated sources.
  4. Enable audit logging – Send all export requests to a centralized logging system (e.g., AWS CloudTrail, ELK stack) with fields: timestamp, client_ip, `user_id` (if authenticated), team_id, and export_type.
  5. Conduct regular red-team exercises – Simulate an attacker attempting to access exports with different team IDs and privacy settings.

NGINX configuration to block unauthenticated exports:

location ~ /team/./income/payments.(json|csv)$ {
 Allow only authenticated requests
auth_request /auth/validate;
auth_request_set $auth_status $upstream_status;

Rate limit to 5 requests per minute per IP
limit_req zone=export_limit burst=10 nodelay;

proxy_pass http://backend;
}

location /auth/validate {
internal;
proxy_pass http://auth-service/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}

AWS WAF rule (JSON):

{
"Name": "BlockUnauthenticatedExports",
"Priority": 10,
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"SearchString": "/team/",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [],
"PositionalConstraint": "CONTAINS"
}
},
{
"ByteMatchStatement": {
"SearchString": "payments.json",
"FieldToMatch": { "UriPath": {} },
"TextTransformations": [],
"PositionalConstraint": "CONTAINS"
}
},
{
"ByteMatchStatement": {
"SearchString": "Cookie",
"FieldToMatch": { "Headers": { "Name": "Cookie" } },
"TextTransformations": [],
"PositionalConstraint": "EXACTLY"
}
}
]
}
},
"Action": { "Block": {} }
}

6. Continuous Monitoring for Privacy Bypass Regression

Once a fix is deployed, privacy bypasses can regress due to schema changes, query optimizations, or new export formats. Implement automated tests that run in CI/CD pipelines to detect regressions before they reach production.

Step-by-step guide for regression testing:

  1. Write unit tests – Create test cases that simulate a donor with “hide giving” enabled and verify that the export endpoint excludes their donations.
  2. Add integration tests – Spin up a staging environment, seed it with test data, and run full export requests against it.
  3. Schedule daily privacy scans – Use a script to fetch exports from a test team and alert if any donation marked private appears.
  4. Monitor HackerOne and similar platforms – Subscribe to disclosure feeds to learn about new privacy bypass techniques.

Python pytest example:

import pytest
from myapp.models import Donation, Donor
from myapp.views import export_payments

@pytest.mark.django_db
def test_export_respects_hide_giving():
donor = Donor.objects.create(username="private_donor", hide_giving=True)
donation = Donation.objects.create(
donor=donor,
team_id=1,
amount=100.00,
current_visibility="private"
)
 Call the export function
export_data = export_payments(team_id=1)
 Assert that the donation is not included
assert donation.id not in [d['donation_id'] for d in export_data]

7. Bug Bounty Disclosure Lifecycle and Responsible Reporting

The Liberapay disclosure followed a responsible timeline: the reporter (Ali Khaled) submitted the issue via HackerOne, the platform triaged and patched it, and the disclosure was made public after the fix was deployed. The $100 bounty reflects the severity—privacy violations are often considered medium-severity, but the potential for OSINT exploitation elevates the risk.

Step-by-step guide for security researchers:

  1. Identify the vulnerability – Map the endpoint, confirm unauthenticated access, and document the privacy bypass.
  2. Create a minimal PoC – Provide a curl command or short script that reproduces the issue without exposing real user data.
  3. Submit via the platform’s bug bounty program – Use HackerOne, Bugcrowd, or the vendor’s own disclosure channel.
  4. Wait for triage and fix – Engage with the security team, provide additional context if needed, and respect the disclosure timeline.
  5. Request public disclosure – After the fix is deployed, request permission to publish a summary. Many programs allow full disclosure after 90 days or once the patch is confirmed.

Recommended disclosure statement template:

“A privacy bypass was identified in the team income export endpoint where unauthenticated requests could retrieve donation details that should have been hidden by donor privacy settings. The issue was caused by the endpoint relying on a frozen visibility value rather than querying the current donation visibility. The vendor has deployed a patch that now applies real-time privacy checks. No customer data was accessed outside of controlled testing.”

What Undercode Say

  • Key Takeaway 1: Never trust stored visibility flags in export endpoints—always re-evaluate against current privacy settings at query time. Frozen fields are a ticking time bomb for privacy violations.
  • Key Takeaway 2: Unauthenticated exports are a high-risk design choice. If an export must be accessible without login, it should be limited to aggregate, non-PII data only.

Analysis: This vulnerability is a textbook example of how “write-once, read-many” caching strategies collide with user privacy expectations. The engineering team likely optimized the export endpoint for performance by denormalizing visibility into the payment table, but they forgot that privacy is a mutable state. The fix—joining with the live donations table and applying current visibility filters—is straightforward but requires a shift in mindset: exports are not static snapshots; they are live views that must respect the latest user preferences. The $100 bounty is modest, but the disclosure has value beyond the monetary reward: it serves as a warning to every platform that handles donations, subscriptions, or any form of financial contribution. If your application has an export feature, audit it today. Ask yourself: “Does this endpoint re-check privacy settings on every request?” If the answer is no, you have a vulnerability waiting to be discovered.

Expected Output

Introduction:

Privacy-preserving donation platforms rely on granular user settings such as “hide my giving” and “hide me from public lists” to protect donor identities from public exposure. However, when export endpoints cache historical visibility states rather than evaluating current privacy preferences, unauthenticated attackers can reconstruct complete donation histories—including donor usernames, exact amounts, currencies, and timestamps—simply by requesting a JSON or CSV export. The Liberapay vulnerability disclosed via HackerOne demonstrates that even mature bug bounty programs can ship endpoints that treat privacy as a one-time snapshot rather than a continuous enforcement boundary.

What Undercode Say:

  • Key Takeaway 1: Never trust stored visibility flags in export endpoints—always re-evaluate against current privacy settings at query time. Frozen fields are a ticking time bomb for privacy violations.
  • Key Takeaway 2: Unauthenticated exports are a high-risk design choice. If an export must be accessible without login, it should be limited to aggregate, non-PII data only.

Prediction:

  • -1 Increased regulatory scrutiny: Privacy regulators (GDPR, CCPA) will increasingly treat export-based privacy bypasses as reportable data breaches, especially when donor identities are exposed without consent.
  • -1 Shift to real-time privacy enforcement: Platforms will abandon denormalized visibility fields in favor of live joins, increasing database query complexity and potentially impacting export performance.
  • +1 Growth of privacy-focused export tooling: Expect new open-source tools and CI/CD checks that automatically audit export endpoints for privacy bypass patterns, reducing the incidence of such flaws.
  • -1 Bug bounty programs will raise bounties for privacy bypasses: As the OSINT value of donor data becomes more widely recognized, bounties for privacy violations will increase, attracting more researcher attention.
  • +1 Improved security awareness: This disclosure will prompt other platforms to audit their own export functionality, leading to a broad reduction in similar vulnerabilities across the industry.

▶️ Related Video (76% Match):

🎯Let’s Practice For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

IT/Security Reporter URL:

Reported By: Hackerone Bug – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky