TL;DR
- Salesforce is not running Log4j the way the scanner assumes. A
${jndi:...}string in a URL path lands as a 404, not a shell. - The fastest way to know is the traffic itself: what was requested, whether the target exists, what came back, and whether any of it touched real data.
- A companion CLI,
sfelf-triage, reads the EventLogFile CSVs and returns a per-IP verdict:BENIGN_SCANNER,SUSPICIOUS, orLIKELY_ABUSE. - It makes no network calls, so it is safe on logs that carry PII, and it needs no org connection.
- It bounds, it does not prove. A
LIKELY_ABUSEverdict is a lead to confirm, never evidence of a breach on its own.
What You'll Learn
- Why a "Log4j exploitation" flag on Salesforce is almost always a scanner
- The specific tells that separate a scan from a real attempt, read straight from the logs
- How to run the triage in one command, and how to read the verdict honestly
- Where the logs stop telling you the truth, and what to reach for next
The Problem
A security alert lands in your inbox. A guest user on your Experience Cloud site, guest@yourorg.force.com, is "performing Aura and Site actions flagged for Log4j exploitation," from a handful of IPs. The alert gives you the IP addresses and nothing else.
So is someone exploiting your org, or is this noise?
If you followed the free EventLogFile baseline guide, you already have the one thing that can answer this: the actual request logs. That post showed you how to capture them. The trouble is reading them. Open a day of Sites and AuraRequest logs for a busy community and you get hundreds of thousands of rows across several CSV files, with the handful of interesting requests buried in ordinary login traffic. Sorting it out by eye does not scale, and it does not survive being handed to the next person on call.
SOQL does not help much either. You cannot join one event type to another, there are no window functions to group a burst of requests into a session, and the big log objects only filter on a date. The raw material is right there, and the platform gives you almost no way to interrogate it.
Common Questions This Article Answers:
- Is my Salesforce org actually vulnerable to Log4Shell if I see these requests?
- How do I tell an automated scanner apart from a real attacker from the logs?
- What do I do with the free EventLogFile logs once an alert names an IP?
Quick Answer
sfelf-triage is on npm, so you can run it without installing anything. Point it at the directory of logs you already pulled, and read the verdict per IP:
npx sfelf-triage analyze ~/.sf/event-baseline/<orgId>
# or install it once: npm i -g sfelf-triage
{
"ip": "203.0.113.10",
"verdict": "BENIGN_SCANNER",
"totalReqs": 5,
"distinctUris": 5,
"families": { "LFI": 1, "Log4Shell": 1, "Oracle-Reports": 2 },
"sfExploitableHits": 0,
"confidence": "No data-return signature observed; content not provable from EventLogFile alone."
}
BENIGN_SCANNER means the traffic matched only generic web-exploit probes, and every response was an error or a stock platform page. An automated scan. No data touched. The rest of this article is how it decides that, and when it decides otherwise.
What the flagged requests actually are
Decode the URLs the scanner sent and the picture resolves fast. A single run usually carries a whole battery of probes, not just Log4j:
- Log4Shell strings, in every obfuscation:
${jndi:ldap://...},${${lower:j}ndi:...}, and so on - Path traversal and local file inclusion:
/app/etc/passwd,..%2f..%2f - Old application CVEs by URL:
/reports/rwservlet(Oracle Reports),struts2-showcase,/actuator,/pentaho/api/ldap - Secret grabs:
/.git/config,/.env,/.env.production - CRLF and header-injection attempts
Two things give it away as a scanner rather than an attacker.
First, the callback host. A real Log4Shell attempt points its jndi lookup at infrastructure the attacker controls, so a successful lookup phones home to them. A scanner points it at a vendor canary so the scanner itself can confirm the hit. If the callback domain is something like *.w.nessus.org or *.oastify.com, you are looking at Tenable Nessus or Burp Suite, not an adversary. That is a fingerprint, not a coincidence.
Second, the targets do not exist. None of struts2-showcase, /actuator, /reports/rwservlet, or /pentaho is a real path on the Salesforce platform. The scanner fires its entire plugin list at every host it finds, blind to what the host actually runs. Salesforce is not a Java app you control, so a ${jndi:...} string in the path is just a path. It routes to the standard 404.
sfelf-triage encodes both of these, and splits its detection catalog in two to make the distinction explicit.
Class 1 is scanner noise, Class 2 is the part that matters
Class 1 is the generic web-exploit corpus: Log4Shell, LFI, RFI and SSRF, Struts2, Spring Actuator, Pentaho, Oracle Reports, exposed .git and .env. These carry sfExploitable: false, because they are inert on Salesforce. A match here means "you are being scanned." Useful to know, not an emergency. The signatures are seeded from the OWASP Core Rule Set, so you are matching against a maintained corpus rather than one analyst's memory of what an attack looks like.
Class 2 is the part that actually matters on this platform. These are Salesforce guest and community abuse patterns, marked sfExploitable: true:
- A guest GraphQL query pulling a collection with
edges, which is a bulk record read rather than a single lookup - A guest invoking an Apex data controller through
ApexActionController, as opposed to the login-page framework actions every visitor triggers - List-view and report reconnaissance
- A file-object read followed within seconds by a
ContentTransferdownload from the same IP, which is the shape of byte exfiltration
You can see the whole catalog without reading the source:
sfelf-triage catalog
sfelf-triage catalog --family Log4Shell
The split is the whole point. A hundred Log4Shell strings that all 404 is a scanner having a normal Tuesday. One guest GraphQL edges read against an object your sharing model was supposed to protect is the thing you actually get out of bed for.
How the verdict is decided
The logic runs in a fixed order, and the order matters:
LIKELY_ABUSEif the IP has anysfExploitable(Class 2) match, or any read-then-download correlation.SUSPICIOUSif it has matches but the responses were not all errors or stock pages, meaning something with a real body size came back on flagged traffic.BENIGN_SCANNERif the matches were Class 1 only and every flagged response was an error or a canned platform page.
To see the reasoning in plain language:
sfelf-triage explain LIKELY_ABUSE
The behavioural signals do not depend on the signature list at all. An IP that hits three thousand distinct URLs in an afternoon is abnormal no matter what those URLs are. That distinct-URL count, the request rate, and the ratio of login-page actions to data-controller actions are computed independently, so an unrecognised probe still shows up as an anomaly instead of hiding in the noise.
Where the logs stop telling the truth
This is the part most tools skip, and skipping it is how you end up over-claiming in an incident report.
EventLogFile records the request and the response metadata. It never records the response body. You cannot read what came back. You can only infer it.
The two signals you get are IS_ERROR and RESPONSE_SIZE. Read together they are usually enough for a scan. Take the classic /etc/passwd probe. A successful local-file-inclusion returns the passwd file, a couple of kilobytes of text. What you actually see is either zero bytes (the request errored) or a 37 KB response, which is the platform's standard branded 404 page. The response is the wrong size, in the wrong direction, for a real leak. And the same 37 KB page comes back for the Log4Shell probe, the Struts probe, and the Oracle Reports probe, identically. Uniform canned sizes across unrelated attacks is the signature of a stock error page, not data.
That inference is solid for scanner probes against endpoints that do not exist. It is not proof of anything for the question that actually matters, which is whether a guest read real records. For that, RESPONSE_SIZE is a proxy, not a fact. ROWS_PROCESSED is not populated for guest UI-API and Aura reads, and a variable, large response on a genuine data controller is a signal, not a receipt.
So when sfelf-triage returns a non-abuse verdict, its confidence note says so out loud: "content not provable from EventLogFile alone." A LIKELY_ABUSE verdict points you at where to actually confirm it: the guest user's access rights at the time, the query shape, any follow-on download, and a deeper relational pull. The tool gives you a lead worth chasing. It does not hand you a conclusion you did not earn.
Running it on sensitive logs
Community logs carry PII by nature, so the analysis path makes no outbound network calls. No telemetry, no live IP or threat-intel lookups. It runs the same under --network=none, and it needs no Salesforce connection, because collection already happened and the CSVs are the whole dataset. You can run it on an air-gapped box holding a copy of the logs and it behaves identically.
The one native dependency is a linear-time regex engine, chosen specifically so that feeding the tool a catastrophic-backtracking payload cannot hang it. A tool that reads hostile input for a living has to survive its own input, and there is a test in the suite that proves it does by running every catalog signature back through the analyzer.
The full loop: capture, then triage
The baseline post gave you collection. This gives you triage. Together they are a complete, no-Shield, no-SIEM incident loop:
# 1. Capture the logs before the one-day free-tier retention drops them
sf audit events pull --target-org myOrg
# 2. Triage them the moment an alert names an IP
sfelf-triage analyze ~/.sf/event-baseline/<orgId>
sfelf-triage reads the plugin's on-disk layout directly, so there is no glue between the two steps.
Frequently Asked Questions
Q: Is my org actually vulnerable to Log4Shell if I see these requests?
A: Not from this vector. The ${jndi:...} strings arrive as URL paths and route to a 404. Salesforce is not executing a Log4j logging call on your community's request path. The requests are worth understanding; the exploit is not landing.
Q: Should I block the IPs?
A: If the scan is unsolicited, blocking the source range at your edge is reasonable, but confirm attribution first. Some of these scans are sanctioned: your own security team, a contracted pentest, or a national CERT running external attack-surface checks. A benign technique is not the same as an authorised one, but neither is it a breach.
Q: What if the verdict is LIKELY_ABUSE?
A: Treat it as a lead. Pull the guest user's effective object and field permissions and the relevant sharing for the time window, check the GraphQL or SOQL query shape, and look for a download that follows a read. The tool tells you where to look; it does not close the case.
Q: Does this need Event Monitoring or Shield?
A: No. It reads the free daily EventLogFile logs, the same ones the baseline post captures. Guest UI-API GraphQL lands in GraphQlQueryExecution, which the free tier exposes.
Q: Can I see what it checks for before I trust it?
A: Yes. sfelf-triage catalog lists every rule with its family and meaning, and sfelf-triage explain describes how each verdict is reached. The tool documents itself so you are not trusting a black box.
Key Takeaways
- A Log4j flag is usually a scanner. On a Salesforce guest user it almost always means a vulnerability scanner spraying inert probes at your public site.
- The tells are in the traffic. Vendor canary callback domains, requests for endpoints that do not exist, and identical canned-page response sizes across unrelated attacks all point to a scan.
- Class 2 is the alarm, not Class 1. A guest bulk read or a read-then-download matters; the generic web noise does not.
- The logs bound, they do not prove. EventLogFile has no response body, so trust the confidence note and confirm a
LIKELY_ABUSEbefore you write it up as one.
What's Next?
Recommended Reading:
- Free Salesforce Event Monitoring: Build a Security Baseline from EventLogFile Without Shield
- Salesforce Guest User Exposure: How sf-audit Grades It by Real Reachability
- Why Salesforce Health Cloud Needs Its Own Security Review
Action Items:
- Confirm you are already capturing the free EventLogFile baseline. If not, start with the baseline post.
- Run
npx sfelf-triage catalogto see exactly what it looks for. - The next time an alert names a guest IP, run
npx sfelf-triage analyzeon the day's logs before you escalate, and read the confidence note.
Using it in an incident, or want it to catch something it does not yet? Leave a comment below. The catalog grows from real cases, and it is useful to know which vectors people are actually seeing.