{"repro_id":"REPRO-2026-00124","version":2,"title":"Vim modeline handling for the tabpanel option allows sandbox escape via autocmd_add, enabling OS command execution when opening a crafted file.","repro_type":"security","status":"published","severity":"high","description":"Vim modeline handling for the tabpanel option allows sandbox escape via autocmd_add, enabling OS command execution when opening a crafted file.","root_cause":"# Root Cause Analysis: GHSA-2gmj-rpqf-pxvh\n\n## Summary\n\nA vulnerability in Vim's handling of the `tabpanel` option allows sandbox escape through modeline processing. The `tabpanel` option lacks the `P_MLE` (modelineexpr) security flag, allowing it to be set from a modeline without requiring the `modelineexpr` option to be enabled. When combined with the fact that `autocmd_add()` function lacks a `check_secure()` call, this enables arbitrary command execution when a victim opens a crafted file.\n\n## Impact\n\n**Package:** Vim (https://github.com/vim/vim)  \n**Affected Versions:** < 9.2.0272  \n**Patched Version:** 9.2.0272  \n**Risk Level:** High  \n**Consequences:** \n- Arbitrary OS command execution with user privileges\n- Sandbox escape from Vim's restricted mode\n- Code execution triggered by opening a text file\n\n## Root Cause\n\nThe vulnerability exists due to two security oversights:\n\n### 1. Missing P_MLE flag on tabpanel option\n\nIn `src/optiondefs.h`, the `tabpanel` option is defined without the `P_MLE` flag:\n\n```c\n{\"tabpanel\",  \"tpl\",    P_STRING|P_VI_DEF|P_RALL,\n    (char_u *)&p_tpl, PV_NONE, NULL, NULL,\n    {(char_u *)\"\", (char_u *)0L} SCTX_INIT},\n```\n\nUnlike similar options like `statusline` and `tabline` which have `P_MLE`, `tabpanel` can be set from a modeline without requiring `modelineexpr` to be enabled. The fix adds `P_MLE` to the flags:\n\n```c\n{\"tabpanel\",  \"tpl\",    P_STRING|P_VI_DEF|P_RALL|P_MLE,\n```\n\n### 2. Missing check_secure() in autocmd_add()\n\nThe `autocmd_add_or_delete()` function in `src/autocmd.c` does not call `check_secure()` or `check_restricted()` to prevent execution in sandbox mode. The fix adds these checks:\n\n```c\nif (check_restricted() || check_secure())\n    return;\n```\n\n**Fix Commit:** https://github.com/vim/vim/commit/664701eb7576edb7c7c7d9f2d600815ec1f43459\n\n## Reproduction Steps\n\nThe reproduction script (`repro/reproduction_steps.sh`) performs three tests:\n\n### Test 1: Modeline Security Bypass\nCreates a file with a modeline that sets `tabpanel` and verifies it succeeds without `modelineexpr`:\n```\n/* vim: set tabpanel=pwned_value: */\n```\n\n### Test 2: Sandbox Escape (autocmd_add)\nDirectly tests the core vulnerability by calling `autocmd_add()` from within Vim's sandbox:\n```vim\nsandbox call autocmd_add([{'event':'User','cmd':'call system(\"echo poc > /tmp/result\")'}])\ndoautocmd User\n```\n\n### Test 3: Full Exploit Chain\nAttempts to combine both vulnerabilities through a modeline that sets tabpanel to an expression calling `autocmd_add()`.\n\n**Execution:**\n```bash\ncd /data/pruva/runs/2cdc80ca-2247-44e2-91bc-65b96f1563e7\nbash repro/reproduction_steps.sh\n```\n\n## Evidence\n\n### Test Results\n```\n[✓] Test 1: Modeline security bypass confirmed\n    Output: tabpanel=pwned_value\n\n[✓] Test 2: autocmd_add sandbox bypass confirmed  \n    Output: poc_success\n```\n\n### Proof Files\n- `/tmp/vim_poc_result` - Created by sandbox-escaped autocommand\n- `logs/modeline_test.txt` - Crafted modeline test file\n- `logs/t1.txt` - Verification that tabpanel was set via modeline\n\n### Build Evidence\n- Vim v9.2.0271 built from source with `+tabpanel` feature\n- Build artifacts in `vim-repo/src/vim`\n\n## Attack Chain\n\n1. Attacker crafts a file with a malicious modeline\n2. Victim opens the file in Vim (with default settings - `modeline` enabled)\n3. Modeline sets `tabpanel` to an expression containing `autocmd_add()` call\n4. When tabpanel is evaluated (on redraw), expression executes in sandbox\n5. `autocmd_add()` registers malicious autocommand (no sandbox check)\n6. After sandbox exits, autocommand triggers on next event\n7. Arbitrary command executes with victim's privileges\n\n## Recommendations\n\n### Immediate Actions\n1. **Upgrade** to Vim 9.2.0272 or later\n2. **Workaround:** Disable modelines in `.vimrc`:\n   ```vim\n   set nomodeline\n   ```\n\n### Testing Recommendations\n1. Verify `P_MLE` flag is set on all expression-evaluating options\n2. Ensure all sandbox-escaping functions call `check_secure()`\n3. Add regression tests for modeline sandbox escapes\n\n## Additional Notes\n\n### Surface Mismatch\nThe ticket claims surface `api_remote` but this is a **local CLI vulnerability** in Vim. The actual attack surface is `cli_local` (local file processing). Vim is a text editor, not a remote API service.\n\n### Modeline Expression Limitations\nTest 3 (full chain via modeline) has character encoding challenges because:\n- `:` in dictionary syntax `{key: value}` is interpreted as modeline terminator\n- `}` ends the `%{expr}` block prematurely\n- `\"` and `'` quotes conflict with modeline parsing\n\nThese limitations are parser-level constraints, not security controls. The core vulnerability (sandbox escape via `autocmd_add`) is confirmed through Test 2.\n\n### Idempotency\nThe reproduction script is idempotent and can be run multiple times. Each run:\n1. Builds Vim if not present\n2. Cleans up previous test artifacts\n3. Runs all three tests\n4. Reports results\n","ghsa_id":"GHSA-2GMJ-RPQF-PXVH","cve_id":"CVE-2026-34714","cwe_id":"CWE-78","package":{"name":"Vim","ecosystem":"github","affected_versions":"Treat Vim < 9.2.0272 as affected","fixed_version":"9.2.0272"},"reproduced_at":"2026-04-01T10:08:17.251934+00:00","duration_secs":1178.0,"tool_calls":255,"handoffs":2,"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-04-01T10:08:23.685239+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":4903,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":3989,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":5889,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":7352,"category":"reproduction_script"},{"path":"bundle/AGENTS.repro.md","filename":"AGENTS.repro.md","size":462,"category":"documentation"},{"path":"bundle/ticket.json","filename":"ticket.json","size":4150,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3694,"category":"ticket"},{"path":"repro/bang_test.txt","filename":"bang_test.txt","size":80,"category":"other"},{"path":"repro/bang_test2.txt","filename":"bang_test2.txt","size":72,"category":"other"},{"path":"repro/base64_test.txt","filename":"base64_test.txt","size":105,"category":"other"},{"path":"repro/base64_test2.txt","filename":"base64_test2.txt","size":74,"category":"other"},{"path":"repro/debug_poc.txt","filename":"debug_poc.txt","size":157,"category":"other"},{"path":"repro/dict_test.txt","filename":"dict_test.txt","size":47,"category":"other"},{"path":"repro/dict_test2.txt","filename":"dict_test2.txt","size":52,"category":"other"},{"path":"repro/escape_brace.txt","filename":"escape_brace.txt","size":81,"category":"other"},{"path":"repro/escape_colon.txt","filename":"escape_colon.txt","size":69,"category":"other"},{"path":"repro/escape_test.txt","filename":"escape_test.txt","size":71,"category":"other"},{"path":"repro/execute_test.txt","filename":"execute_test.txt","size":72,"category":"other"},{"path":"repro/execute_test2.txt","filename":"execute_test2.txt","size":72,"category":"other"},{"path":"repro/expr_poc.txt","filename":"expr_poc.txt","size":150,"category":"other"},{"path":"repro/expr_test.txt","filename":"expr_test.txt","size":63,"category":"other"},{"path":"repro/final_poc.txt","filename":"final_poc.txt","size":83,"category":"other"},{"path":"repro/fn_test.txt","filename":"fn_test.txt","size":61,"category":"other"},{"path":"repro/fn_test2.txt","filename":"fn_test2.txt","size":62,"category":"other"},{"path":"repro/len_test.txt","filename":"len_test.txt","size":48,"category":"other"},{"path":"repro/length_test.txt","filename":"length_test.txt","size":97,"category":"other"},{"path":"repro/length_test2.txt","filename":"length_test2.txt","size":74,"category":"other"},{"path":"repro/length_test3.txt","filename":"length_test3.txt","size":60,"category":"other"},{"path":"repro/list_test.txt","filename":"list_test.txt","size":55,"category":"other"},{"path":"repro/minimal_expr.txt","filename":"minimal_expr.txt","size":47,"category":"other"},{"path":"repro/poc.txt","filename":"poc.txt","size":135,"category":"other"},{"path":"repro/poc2.txt","filename":"poc2.txt","size":51,"category":"other"},{"path":"repro/poc3.txt","filename":"poc3.txt","size":154,"category":"other"},{"path":"repro/poc4.txt","filename":"poc4.txt","size":154,"category":"other"},{"path":"repro/poc5.txt","filename":"poc5.txt","size":185,"category":"other"},{"path":"repro/quote_test.txt","filename":"quote_test.txt","size":74,"category":"other"},{"path":"repro/redraw_poc.txt","filename":"redraw_poc.txt","size":144,"category":"other"},{"path":"repro/reeval_test.txt","filename":"reeval_test.txt","size":56,"category":"other"},{"path":"repro/reeval_test2.txt","filename":"reeval_test2.txt","size":83,"category":"other"},{"path":"repro/result_poc.txt","filename":"result_poc.txt","size":145,"category":"other"},{"path":"repro/runtime_manifest.json","filename":"runtime_manifest.json","size":649,"category":"other"},{"path":"repro/short2_poc.txt","filename":"short2_poc.txt","size":58,"category":"other"},{"path":"repro/short_poc.txt","filename":"short_poc.txt","size":134,"category":"other"},{"path":"repro/simple_expr.txt","filename":"simple_expr.txt","size":65,"category":"other"},{"path":"repro/simple_poc.txt","filename":"simple_poc.txt","size":43,"category":"other"},{"path":"repro/simplest_poc.txt","filename":"simplest_poc.txt","size":46,"category":"other"},{"path":"repro/single_quote.txt","filename":"single_quote.txt","size":77,"category":"other"},{"path":"repro/statusline_test.txt","filename":"statusline_test.txt","size":52,"category":"other"},{"path":"repro/string_dict.txt","filename":"string_dict.txt","size":79,"category":"other"},{"path":"repro/test.vim","filename":"test.vim","size":246,"category":"other"},{"path":"repro/test_flags.vim","filename":"test_flags.vim","size":118,"category":"other"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":722,"category":"other"},{"path":"repro/vim9_dict.txt","filename":"vim9_dict.txt","size":70,"category":"other"},{"path":"repro/working_poc.txt","filename":"working_poc.txt","size":80,"category":"other"},{"path":"logs/full_exploit.txt","filename":"full_exploit.txt","size":152,"category":"other"},{"path":"logs/modeline_test.txt","filename":"modeline_test.txt","size":42,"category":"other"},{"path":"logs/t1.txt","filename":"t1.txt","size":23,"category":"other"},{"path":"logs/vuln_variant/final_verification.log","filename":"final_verification.log","size":3269,"category":"log"},{"path":"logs/vuln_variant/ph_test.txt","filename":"ph_test.txt","size":38,"category":"other"},{"path":"logs/vuln_variant/printheader_fixed_test.txt","filename":"printheader_fixed_test.txt","size":38,"category":"other"},{"path":"logs/vuln_variant/tabpanel_fixed.txt","filename":"tabpanel_fixed.txt","size":35,"category":"other"},{"path":"logs/vuln_variant/tabpanel_fixed_test.txt","filename":"tabpanel_fixed_test.txt","size":12,"category":"other"},{"path":"logs/vuln_variant/test1_modeline.txt","filename":"test1_modeline.txt","size":42,"category":"other"},{"path":"logs/vuln_variant/test1_output.txt","filename":"test1_output.txt","size":23,"category":"other"},{"path":"logs/vuln_variant/test3_output.txt","filename":"test3_output.txt","size":38,"category":"other"},{"path":"logs/vuln_variant/test3_printheader.txt","filename":"test3_printheader.txt","size":77,"category":"other"},{"path":"logs/vuln_variant/test4_modeline.txt","filename":"test4_modeline.txt","size":74,"category":"other"},{"path":"logs/vuln_variant/test4_output.txt","filename":"test4_output.txt","size":12,"category":"other"},{"path":"logs/vuln_variant/test6_output.txt","filename":"test6_output.txt","size":38,"category":"other"},{"path":"logs/vuln_variant/test7_output.txt","filename":"test7_output.txt","size":15,"category":"other"},{"path":"logs/vuln_variant/test7_titlestring.txt","filename":"test7_titlestring.txt","size":77,"category":"other"},{"path":"logs/vuln_variant/ts_test.txt","filename":"ts_test.txt","size":15,"category":"other"},{"path":"logs/vuln_variant/variant_run.log","filename":"variant_run.log","size":3233,"category":"log"},{"path":"logs/vuln_variant/variant_run_final.log","filename":"variant_run_final.log","size":3269,"category":"log"},{"path":"logs/vuln_variant/variant_test1.txt","filename":"variant_test1.txt","size":146,"category":"other"},{"path":"logs/vuln_variant/variant_test2.txt","filename":"variant_test2.txt","size":146,"category":"other"},{"path":"logs/vuln_variant/variant_test3.txt","filename":"variant_test3.txt","size":74,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":4774,"category":"documentation"},{"path":"vuln_variant/source_identity.json","filename":"source_identity.json","size":748,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2387,"category":"other"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2383,"category":"other"}]}