SecurityWeek reports that more than 1,000 charities were impacted by the Beacon CRM data breach, with the likely root cause being a compromised AWS access key exposed in publicly available JavaScript build artifacts. This is not a niche frontend hygiene issue. If a long-lived AWS credential shipped in a browser-downloadable bundle, every defender should assume secret scanners found it and tested its permissions within minutes.
No CVE identifier is provided in the source material, and none should be invented. The present-day lesson is the continuing exploitation pattern: public build artifacts, source maps, static JavaScript, CI/CD logs, and object-storage paths remain reliable places for attackers to harvest cloud credentials. The affected platform is Beacon CRM, a SaaS CRM used by charities and nonprofit organizations. The risk extends beyond donor PII: depending on IAM scope, a leaked key may permit S3 reads, snapshot downloads, RDS or DynamoDB enumeration, SES abuse, KMS decrypt attempts, CloudTrail visibility gaps, and persistence through new access keys or role trust changes.
Treat this as an active credential-compromise event. Your first priorities are to confirm whether any secret could be public, rotate aggressively, and review AWS CloudTrail data-plane events for access that did not originate from expected CI/CD roles, corporate egress, or SaaS provider infrastructure.
Technical analysis
The reported attack chain is straightforward and highly repeatable:
- A build pipeline produces browser artifacts such as hashed JavaScript bundles, source maps, configuration files, or static assets under a public CDN, S3 bucket, or web root.
- A secret is bundled by mistake: an AWS access key ID beginning with AKIA or ASIA, a session token, an .env file, a config constant, a source map containing original environment values, or a build log reachable from the same public path.
- External scanners continuously crawl common paths, JavaScript bundles, source maps, and bucket indexes for high-entropy tokens and cloud key patterns.
- The actor validates the credential with low-noise calls such as sts:GetCallerIdentity, then enumerates permissions using iam:List*, s3:ListAllMyBuckets, or directly attempts data-plane reads.
- Impact is determined by IAM blast radius: read-only S3 access can expose exports and backups; wildcard permissions can enable exfiltration, destructive actions, SES phishing infrastructure, or persistence via iam:CreateAccessKey and iam:AttachUserPolicy.
Defenders should distinguish three exposure states:
- Secret present in deployed public artifact: critical; rotate immediately and investigate historical logs.
- Secret only in private build system: severe but contained; still rotate if the artifact was ever public, cached, mirrored, or logged.
- Secret in dependency or developer workstation: investigate build provenance, package scripts, and local tooling before declaring cloud impact.
Exploitation status from the reported item is confirmed breach impact, not theoretical. Even where your organization is not a Beacon customer, the technique is actively used against any public web property that emits secrets into client-side artifacts.
Detection and response
The detections below are intentionally narrow. They favor high-confidence signals around cloud credential use and accidental artifact publication rather than generic JavaScript access, which would be noisy.
---
title: AWS IAM User Long-Lived Key Used for Data-Plane Access
id: 9f6b2a51-4d3c-4e7a-8b11-6c2d9f41a7b2
status: experimental
description: Detects IAM user credentials, rather than assumed roles or federated identities, performing high-risk AWS enumeration or data access consistent with a leaked access key.
references:
- https://attack.mitre.org/techniques/T1078/004/
- https://attack.mitre.org/techniques/T1530/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.initial_access
- attack.collection
- attack.t1078.004
- attack.t1530
logsource:
product: aws
service: cloudtrail
detection:
selection_identity:
userIdentity.type: IAMUser
selection_events:
eventName:
- GetCallerIdentity
- ListAllMyBuckets
- ListBuckets
- GetObject
- ListObjects
- ListObjectsV2
- GetBucketPolicy
- GetBucketAcl
- GenerateDbAuthToken
- CreateAccessKey
- AttachUserPolicy
- PutUserPolicy
condition: selection_identity and selection_events
falsepositives:
- Legacy break-glass IAM users and poorly modernized automation
level: high
---
title: AWS S3 Bucket Made Public or Exfiltration Guardrail Weakened
id: 3d8e1c70-2a94-4bb9-91f0-7c5aa21d90e4
status: experimental
description: Detects S3 control-plane changes that can expose objects after credential compromise, including public policy changes and deletion of encryption or logging controls.
references:
- https://attack.mitre.org/techniques/T1537/
- https://attack.mitre.org/techniques/T1565/001/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.exfiltration
- attack.defense_evasion
- attack.t1537
- attack.t1565.001
logsource:
product: aws
service: cloudtrail
detection:
selection:
eventName:
- PutBucketPolicy
- PutBucketAcl
- DeleteBucketPolicy
- PutBucketPublicAccessBlock
- DeleteBucketEncryption
- PutBucketLogging
- DeleteBucket
- PutLifecycleConfiguration
errorCode: success
condition: selection
falsepositives:
- Approved infrastructure-as-code changes during deployment windows
level: critical
---
title: Secret Material Written to Public Web or Build Output Paths
id: 5b1f8d42-0c7e-4f8a-a3d9-21e8b6c54aa9
status: experimental
description: Detects build or deployment processes writing files with names commonly associated with secrets into public web roots or distributable frontend output directories.
references:
- https://attack.mitre.org/techniques/T1552/001/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.credential_access
- attack.t1552.001
logsource:
category: file_event
product: linux
detection:
selection_paths:
TargetFilename|contains:
- '/var/www/'
- '/srv/www/'
- '/usr/share/nginx/'
- '/dist/'
- '/build/'
- '/public/'
- '/static/js/'
selection_names:
TargetFilename|endswith:
- '.env'
- '.env.production'
- 'config.js'
- 'runtime-config.json'
- '.js.map'
condition: selection_paths and selection_names
falsepositives:
- Legitimate frontend config files that contain only non-secret runtime values
level: medium
// Hunt endpoint/build-agent egress to AWS endpoints from tools that commonly participate in frontend builds or manual secret testing.
// Tune the egress allowlist to your approved CI/CD subnets and SaaS build providers.
DeviceNetworkEvents
| where TimeGenerated > ago(7d)
| where RemoteUrl has_any ("amazonaws.com", "aws.amazon.com") or RemoteIp has_any ("3.", "13.", "18.", "34.", "44.", "52.", "54.")
| where InitiatingProcessFileName in~ ("node.exe", "node", "npm.exe", "npm", "pnpm.exe", "yarn.exe", "git.exe", "curl.exe", "wget.exe", "python.exe", "powershell.exe", "bash", "sh")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl, RemoteIp, RemotePort, ReportId
| order by TimeGenerated desc;
// CloudTrail events forwarded as CEF/syslog can land in CommonSecurityLog. Field names vary by connector; pivot on Message when normalized fields are unavailable.
CommonSecurityLog
| where TimeGenerated > ago(14d)
| where Message has_any ("GetCallerIdentity", "ListAllMyBuckets", "CreateAccessKey", "AttachUserPolicy", "PutBucketPolicy", "GetObject", "ListObjectsV2")
| extend EventName = extract("eventName[=: ]+([A-Za-z0-9]+)", 1, Message)
| extend UserType = extract("userIdentity[^\n]*type[=: ]+([A-Za-z]+)", 1, Message)
| extend SourceIp = extract("sourceIPAddress[=: ]+([0-9a-fA-F:.]+)", 1, Message)
| where UserType =~ "IAMUser" or EventName in~ ("CreateAccessKey", "AttachUserPolicy", "PutBucketPolicy", "PutBucketAcl")
| summarize Count = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by EventName, UserType, SourceIp, DeviceVendor, DeviceProduct
| order by Count desc;
-- Hunt Linux web roots and CI/CD build output for public artifacts likely to contain leaked cloud secrets.
-- Deploy to web servers, build agents, and artifact staging hosts. Review matches before rotation; do not paste secrets into tickets.
SELECT FullPath, Size, Mtime,
hash(path=FullPath) AS FileHash
FROM glob(globs=[
'/var/www/**/*.env*',
'/var/www/**/*.js.map',
'/var/www/**/config.js',
'/srv/www/**/*.env*',
'/usr/share/nginx/**/*.js.map',
'/opt/**/dist/**/*.js',
'/opt/**/build/**/*.js',
'/home/*/**/dist/**/*.js',
'/home/*/**/build/**/*.js'
])
WHERE Size < 20000000
AND (FullPath =~ '(?i)(env|secret|config|sourcemap|map|static/js|dist|build)')
#!/usr/bin/env bash
# Defensive verification for leaked AWS keys in public artifacts. Run on web/build hosts; requires awscli v2 and jq for IAM checks.
set -euo pipefail
REPORT="./aws_artifact_exposure_$(date -u +%Y%m%dT%H%M%SZ).txt"
ROOTS=("/var/www" "/srv/www" "/usr/share/nginx/html" "/opt" "$HOME")
{
echo "== Public artifact secret scan =="
for root in "${ROOTS[@]}"; do
[ -d "$root" ] || continue
echo "-- root: $root"
find "$root" -type f \( -name '*.js' -o -name '*.map' -o -name '.env*' -o -name 'config.js' -o -name 'runtime-config.json' \) -size -20M -print0 2>/dev/null \
| xargs -0 grep -HEn --binary-files=without-match -e 'AKIA[0-9A-Z]{16}' -e 'ASIA[0-9A-Z]{16}' -e 'aws_secret_access_key' -e 'AWS_SESSION_TOKEN' -e 'aws_access_key_id' 2>/dev/null || true
done
echo
echo "== AWS identity and access-key inventory =="
aws sts get-caller-identity || true
aws iam list-users --query 'Users[].UserName' --output text 2>/dev/null | while read -r u; do
aws iam list-access-keys --user-name "$u" --output json 2>/dev/null | jq -r --arg u "$u" '.AccessKeyMetadata[] | [$u,.AccessKeyId,.Status,.CreateDate] | @tsv'
done
echo
echo "== S3 public access posture =="
aws s3control get-public-access-block --account-id "$(aws sts get-caller-identity --query Account --output text 2>/dev/null)" 2>/dev/null || true
aws s3api list-buckets --query 'Buckets[].Name' --output text 2>/dev/null | tr '\t' '\n' | while read -r b; do
[ -n "$b" ] || continue
echo "bucket=$b"
aws s3api get-public-access-block --bucket "$b" 2>/dev/null || echo "no account/bucket public access block output for $b"
aws s3api get-bucket-policy-status --bucket "$b" 2>/dev/null || true
done
} | tee "$REPORT"
echo "Report written to $REPORT. Rotate/delete any exposed key immediately; do not rely on removing the public file alone."
Immediate response checklist
- Contain the credential. Disable, then delete exposed AWS access keys. Do not wait for a full forensic timeline before disabling a key that was publicly retrievable. Preserve evidence first where practical: copy CloudTrail, S3 access logs, CloudFront logs, build logs, deployment manifests, and artifact hashes.
- Rotate dependent secrets. Database passwords, API tokens, SES SMTP credentials, third-party webhooks, and CRM connector secrets may have been reachable from the same IAM principal, S3 objects, parameter store entries, or exported data.
- Establish blast radius. Enumerate exactly what the compromised identity could do using sts:GetCallerIdentity, iam:ListUserPolicies, iam:ListAttachedUserPolicies, iam:SimulatePrincipalPolicy, CloudTrail event history, S3 access logs, GuardDuty findings, Security Hub, and Macie where enabled.
- Remove secrets from frontend output. Fail builds on gitleaks, trufflehog, or equivalent detectors. Block .env, secrets, private source maps containing credentials, and build logs from public paths. Publish source maps only to authenticated error telemetry platforms unless there is a documented need.
- Eliminate long-lived IAM users for workloads. Prefer IAM roles, OIDC federation for CI/CD, instance roles, ECS/Lambda task roles, and short-lived credentials. Where IAM users remain, require MFA for console use, key age limits, and explicit owner justification.
- Constrain stolen keys even if rotation is delayed. Apply least privilege, SCP denies for sensitive services, aws:SourceIp conditions where deterministic egress exists, aws:ViaAWSService where appropriate, S3 Block Public Access, default encryption, object ownership controls, and explicit denies for KMS decrypt outside expected roles.
- Monitor the exfil paths. Enable CloudTrail management and data events for high-value S3 buckets and RDS where justified, S3 server access logging or CloudTrail data events, GuardDuty, Security Hub aggregation, ECR/image scan findings, and billing anomaly alerts.
- Notify with precision. Beacon CRM customers should confirm processor timelines, affected records, donor categories, payment data scope, and regulator obligations. Charities may have duties under UK GDPR/EU GDPR, local charity regulators, state breach notification laws, PCI-DSS if cardholder data is in scope, and HIPAA only if the nonprofit handles protected health information in a covered context.
Longer-term hardening
Make secret exposure a build failure, not a code-review comment. Add pre-commit scanning, CI/CD scanning, artifact scanning after bundling, and canary AWS keys in public repositories to detect crawl-to-use time. Keep browser bundles free of privileged configuration; the frontend is public by design. Review CDN caching because deleting a leaked file does not purge copies from edge caches, search engines, archives, or scanner databases.
For SaaS dependencies such as CRMs, require evidence in procurement and ongoing reviews: breach notification SLAs, subprocessor lists, support for customer-managed keys or dedicated tenancy where needed, audit log export, SSO/SCIM, IP allowlisting for exports, and clear separation between application secrets and client-delivered artifacts.
Related Resources
Security Arsenal Incident Response Services AlertMonitor Platform Book a SOC Assessment incident-response Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.