Heatmap's ingestion S3 bucket was publicly writable
TL;DR
In November 2025, while I was testing a Heatmap implementation, I found that the config file loaded by the ingestion JavaScript declared their current active customers. Looking closer at the same config endpoint, I found an AWS S3 bucket that allowed public read and write. Anonymous requests could list, download, upload, and delete objects. Heatmap’s tracking script also uploaded analytics events from visitors’ browsers to predictable keys in the same bucket without auth, which made the writable bucket much riskier.
I reported the issue privately starting 26 November 2025 and followed up several times when I couldn’t get a secure channel for the more sensitive details. The initial config-file issue was addressed quickly. After a few emails and a proof-of-concept call, they locked down the bucket.
Heatmap is a website analytics and eCommerce optimization platform. I came across the issue during a routine implementation review, starting with a client-side config file that exposed more than it should have.
The initial report was sent in November 2025. After the fix, I confirmed that the bucket described here was no longer publicly accessible. I have not re-tested the broader service or the client-side ingestion path since, so nothing below should be read as a description of how Heatmap works today.
Severity
Anonymous read/write/delete access to production analytics storage is enough to cause data exposure, tampering, and disruption. I assessed the issue as:
- CVSS 4.0: 9.9 / 10 (Critical)
- CVSS 3.1: 10.0 / 10 (Critical)
- Vector (3.1):
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Timeline
-
While assessing an implementation, I found that Heatmap's ingestion JavaScript loaded a
recording_users.jsonconfig file, which at the time declared current active customers. I reported that initial finding to Heatmap support. -
Received a reply confirming a fix for that file. Looking closer at
https://c.heatmap.com/configs, I found configuration details that pointed to an S3 bucket, then confirmed the bucket was publicly readable and writable. I asked for a secure channel for the more severe findings, including proof-of-concept evidence. -
Sent additional follow-ups through support. I was referred to Heatmap's CTO and asked for a secure channel or
security@contact so I could hand over the sensitive details safely. No secure channel was provided. -
Disclosed the higher-risk client-side ingestion details verbally and privately to an engineer responsible for the ingestion solution.
-
After a quick call, I sent reproduction commands demonstrating the unauthenticated S3 access and walked them through the exact steps and configuration needed to lock the bucket down.
-
Hours after the call, Heatmap locked down the S3 bucket.
-
90-day disclosure window ended, report published.
-
No bounty has been paid.
How I found it
The initial finding was a recording_users.json file loaded by Heatmap’s ingestion JavaScript from its config endpoint. At the time, it declared current active customers. I reported that file during an implementation call and was later told the exposed details had been removed or redacted.
Looking closer at https://c.heatmap.com/configs, I found that the config endpoint returned environment/configuration details, including the S3 bucket URL. That led me to the publicly readable and writable bucket described below.
The bucket
The S3 bucket was reachable through unauthenticated AWS CLI requests using --no-sign-request. During a disclosure call with Heatmap staff, and with their permission, I used a minimal proof of concept to confirm that it was readable and writable:
aws s3 ls s3://<redacted-bucket> --no-sign-request
# 2025-10-25 11:33:04 2.2 KiB hmr-mutations/type-5/2025-10-25/<redacted-customer>/7634445124675013.json.gz
# 2025-10-26 06:13:15 1.1 KiB hmr-mutations/type-5/2025-10-26/<redacted-customer>/0144967669810347.json.gz
# 2025-10-26 06:13:21 14.9 KiB hmr-mutations/type-5/2025-10-26/<redacted-customer>/0896839620591796.json.gz
# 2025-10-26 21:17:56 229.2 KiB hmr-mutations/type-5/2025-10-26/<redacted-customer>/4391219122417897.json.gz
# 2025-10-27 00:18:00 230.5 KiB hmr-mutations/type-5/2025-10-26/<redacted-customer>/6102095132193214.json.gz
# 2025-10-26 11:31:22 171.1 KiB hmr-mutations/type-5/2025-10-26/<redacted-customer>/0215662676764356.json.gz
aws s3 cp test.txt s3://<redacted-bucket> --no-sign-request
# upload: ./test.txt to s3://<redacted-bucket>/test.txt
aws s3 ls s3://<redacted-bucket>/test.txt --no-sign-request
# 2025-11-27 11:58:52 8 test.txt
curl "https://<redacted-bucket>.s3.<region>.amazonaws.com/test.txt"
# testing
aws s3 rm s3://<redacted-bucket>/test.txt --no-sign-request
# delete: s3://<redacted-bucket>/test.txt
I also confirmed enumeration and download behavior with unauthenticated requests. The test file was harmless, and I removed it during the same call after the check was complete. I did not copy or retain customer data beyond what I needed to understand and report the issue.
Root cause
The S3 bucket had AWS Block Public Access effectively disabled for the exposure in question, and its policy allowed anonymous access to core actions:
s3:ListBucket- enumerate contentss3:GetObject- download filess3:PutObject- upload filess3:DeleteObject- delete files
My guess is that public PutObject access may have been intentional to support client-side ingestion, but public read, list, and delete access were not. Even if writes were by design, accepting anonymous, unauthenticated requests without scoped credentials, signed requests, or rate limiting left the bucket exposed.
Why the write access mattered
Public reads are bad; public writes are worse.
Anonymous GetObject and ListBucket access can expose analytics exports, session-related data, URLs, page content, and operational metadata. Depending on how recording and masking were configured, mutation data could also include values entered into forms, such as email addresses submitted to newsletter signup forms. If a customer used the service on checkout or payment pages, the same risk could extend to more sensitive checkout form data. Anonymous PutObject and DeleteObject access add integrity and availability risk: files can be overwritten, planted, or removed. In an analytics system, that can mean poisoned metrics, corrupted records, unexpected downstream behavior, or a storage/billing denial-of-service problem.
The exposed data appeared to include large volumes of analytics objects, including JSON exports related to hmr-mutations. Based on what I observed, the bucket contained on the order of millions of objects.
The higher-risk detail: client-side writes
The most serious issue was how analytics data was uploaded. Heatmap’s tracking script sent analytics events (including records of page changes and visitor interactions) from visitors’ browsers straight to predictable locations in the same writable bucket, with no login or secret required.
That made the problem much bigger:
- Every visitor on a Heatmap-tracked site could, in effect, trigger writes to the company’s central analytics storage.
- Anyone who understood the request pattern could spoof or poison events from their own browser or a simple script, without AWS credentials.
- The result could be corrupted revenue attribution, inflated or zeroed metrics, or forced storage/billing abuse.
I withheld the full request-pattern details from written communication and disclosed them verbally to an engineer responsible for the ingestion solution on 1 December 2025.
Reporting experience
The narrow config-file exposure was acknowledged and partially addressed quickly, which I appreciated. It showed the report had reached someone who could act.
The critical bucket exposure and the client-side ingestion path were a different matter. I wanted to share the full technical picture, but I didn’t feel comfortable sending a reproducible, high-risk write path over regular email without a secure channel. I asked for one, offered to walk through the sensitive details live on a short call instead, and followed up politely. I never received that secure channel or formal confirmation covering the full scope. After the fix, though, I did confirm that the bucket itself was no longer publicly accessible.
Heatmap did not have a formal vulnerability disclosure or bounty program at the time. We discussed recognition after the fix.
Recommendations
- Enable S3 Block Public Access at the account and bucket levels, and treat exceptions as reviewed security decisions.
- Remove anonymous bucket policies. No production analytics bucket should allow anonymous
ListBucket,GetObject,PutObject, orDeleteObject. - Do not accept direct unauthenticated client writes to a shared bucket. Use signed, scoped, short-lived upload credentials or a server-side ingestion endpoint with auth, validation, rate limiting, and abuse controls.
- Make object keys unguessable and scoped. Predictable keys make spoofing and enumeration easier.
- Enable S3 server access logging and monitoring. Integrate alerts for public access, policy changes, unusual
PUT/DELETEvolume, and unexpected object patterns. - Audit buckets continuously. Tools such as AWS IAM Access Analyzer, Security Hub, and Macie can catch public exposure and sensitive-data risk earlier.
- Assess notification and compliance obligations. Exposed session/analytics data may trigger contractual or regulatory duties depending on the data involved.
- Publish a security disclosure path. Add
/.well-known/security.txtand a monitored security contact so reports reach the right team quickly.
Lessons learned
S3 writability turns a leak into an active attack surface. Reading data is only one failure mode; tampering can quietly destroy trust in the data.
Client-side analytics paths need server-side controls. If a browser can write directly to storage, assume an attacker can too.
A partial fix is not closure. Removing URLs from a config file helped, but it did not address the underlying storage permissions or ingestion design.
Security reports need a safe channel. The absence of a secure intake path slowed the most important part of the process: getting complete details to the people who can fix the problem.
Credit
Thanks to the Heatmap support staff who responded quickly to the initial config-file issue, and to the people who took time to discuss the report with me. I would have preferred to close the loop through a secure, acknowledged remediation process, but I’m glad the first issue was handled promptly. If anyone at Heatmap wants to share clarification, remediation confirmation, or coordinate on identifiers, I’m happy to update this post.