{"article":{"slug":"posix-wordexp-3-is-dangerous","title":"POSIX wordexp(3) is dangerous","subtitle":null,"summary":"yossarian explains why the POSIX wordexp(3) API, often recommended for tilde expansion in C, enables command substitution by default, and why even the WRDE_NOCMD flag is unreliable on musl and macOS, so it should never be used on untrusted input.","content_type":"tutorial","language":"en","canonical_url":"https://yossarian.net/til/post/posix-wordexp-3-is-dangerous/","author":{"name":"yossarian","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"yossarian.net","url":"https://yossarian.net/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"C","slug":"c","url":"https://listedarticles.com/topics/c"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":258,"reading_minutes":1,"published_at":"2026-10-25T00:00:00.000Z","added_at":"2026-10-25T17:10:24.444Z","updated_at":"2026-10-25T17:10:24.444Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/posix-wordexp-3-is-dangerous","markdown_url":"https://listedarticles.com/articles/posix-wordexp-3-is-dangerous.md","example":false,"citation":"yossarian, yossarian.net. \"POSIX wordexp(3) is dangerous.\" 25 Oct 2026. https://yossarian.net/til/post/posix-wordexp-3-is-dangerous/ (all-rights-reserved)","access":{"human_view":"full","full_text_available":true,"source_url":"https://yossarian.net/til/post/posix-wordexp-3-is-dangerous/"},"body_markdown":"[`wordexp(3)`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/wordexp.html) is a POSIX API for performing shell-style expansions from C.\n\nIt's often cited/recommended as a way to do tilde expansion (e.g. `~/foo` to `/home/me/foo`):\n\n```\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <wordexp.h>\n\nint expand_tilde(const char *input, char **output) {\n    wordexp_t p;\n    int ret = wordexp(input, &p, 0);\n    if (ret == 0) {\n        *output = strdup(p.we_wordv[0]);\n        wordfree(&p);\n    }\n    return ret;\n}\n\nint main(void) {\n    char *expanded = NULL;\n    expand_tilde(\"~/foo\", &expanded);\n    printf(\"expanded: %s\\n\", expanded);\n    return 0;\n}\n```\n\nHowever, `wordexp(3)` is also dangerous! That last `0` argument is for the *flags* parameter.\nBy default, all expansion features are enabled, **including command expansion**:\n\n```\nexpand_tilde(\"$(whoami)/foo\", &expanded);\n```\n\nor:\n\n```\nexpand_tilde(\"$(cat /etc/passwd)/foo\", &expanded);\n```\n\nDisabling this requires passing [`WRDE_NOCMD`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/wordexp.html#:~:text=WRDE_NOCMD,Substitution%2C%20is%20requested.) as a flag. Unfortunately, this\npalliative at best in some libc implementations:\n\n- [musl implements `wordexp(3)`](https://git.musl-libc.org/cgit/musl/tree/src/misc/wordexp.c) by delegating to the system shell. It adds some\n  perfunctory scanning for command substitutions before delegating, but\n  fails to match some patterns that the underlying shell (like GNU Bash) can treat\n  as valid command substitutions.\n- [macOS's libc implements `wordexp(3)`](https://github.com/apple-oss-distributions/Libc/blob/71bbe350ab79eef58113991d817ccc6165061a64/gen/FreeBSD/wordexp.c#L201) in a similar manner, delegating to\n  `/usr/lib/system/wordexp-helper`[1](#fn-perl). According to [Julian Squires](https://www.cipht.net)\n  this had the same bypass as musl [as recently as 2023](https://www.cipht.net/2023/11/21/worried-by-wordexp.html), although I didn't bother\n  testing whether it's still effective now that the default macOS shell is `zsh`\n  instead of `bash`.\n\nBottom line: don't use `wordexp(3)` on untrusted inputs, even with `WRDE_NOCMD`.\n\n1. Previously, macOS's `wordexp(3)` would shell out to Perl. `wordexp-helper` appears to be the \"modern\" solution. [↩](#fnref-perl)\n","body_html":"<p><a href=\"https://pubs.opengroup.org/onlinepubs/9799919799/functions/wordexp.html\" rel=\"nofollow ugc noopener\"><code>wordexp(3)</code></a> is a POSIX API for performing shell-style expansions from C.</p>\n<p>It&#39;s often cited/recommended as a way to do tilde expansion (e.g. <code>~/foo</code> to <code>/home/me/foo</code>):</p>\n<pre><code>#include &lt;stdio.h&gt;\n#include &lt;stdlib.h&gt;\n#include &lt;string.h&gt;\n#include &lt;wordexp.h&gt;\n\nint expand_tilde(const char *input, char **output) {\n    wordexp_t p;\n    int ret = wordexp(input, &amp;p, 0);\n    if (ret == 0) {\n        *output = strdup(p.we_wordv[0]);\n        wordfree(&amp;p);\n    }\n    return ret;\n}\n\nint main(void) {\n    char *expanded = NULL;\n    expand_tilde(&quot;~/foo&quot;, &amp;expanded);\n    printf(&quot;expanded: %s\\n&quot;, expanded);\n    return 0;\n}</code></pre>\n<p>However, <code>wordexp(3)</code> is also dangerous! That last <code>0</code> argument is for the <em>flags</em> parameter.\nBy default, all expansion features are enabled, <strong>including command expansion</strong>:</p>\n<pre><code>expand_tilde(&quot;$(whoami)/foo&quot;, &amp;expanded);</code></pre>\n<p>or:</p>\n<pre><code>expand_tilde(&quot;$(cat /etc/passwd)/foo&quot;, &amp;expanded);</code></pre>\n<p>Disabling this requires passing <a href=\"https://pubs.opengroup.org/onlinepubs/9799919799/functions/wordexp.html#:~:text=WRDE_NOCMD,Substitution%2C%20is%20requested.\" rel=\"nofollow ugc noopener\"><code>WRDE_NOCMD</code></a> as a flag. Unfortunately, this\npalliative at best in some libc implementations:</p>\n<ul><li><p><a href=\"https://git.musl-libc.org/cgit/musl/tree/src/misc/wordexp.c\" rel=\"nofollow ugc noopener\">musl implements <code>wordexp(3)</code></a> by delegating to the system shell. It adds some</p><p>perfunctory scanning for command substitutions before delegating, but\nfails to match some patterns that the underlying shell (like GNU Bash) can treat\nas valid command substitutions.</p></li><li><p><a href=\"https://github.com/apple-oss-distributions/Libc/blob/71bbe350ab79eef58113991d817ccc6165061a64/gen/FreeBSD/wordexp.c#L201\" rel=\"nofollow ugc noopener\">macOS&#39;s libc implements <code>wordexp(3)</code></a> in a similar manner, delegating to</p><p><code>/usr/lib/system/wordexp-helper</code><a href=\"#fn-perl\">1</a>. According to <a href=\"https://www.cipht.net\" rel=\"nofollow ugc noopener\">Julian Squires</a>\nthis had the same bypass as musl <a href=\"https://www.cipht.net/2023/11/21/worried-by-wordexp.html\" rel=\"nofollow ugc noopener\">as recently as 2023</a>, although I didn&#39;t bother\ntesting whether it&#39;s still effective now that the default macOS shell is <code>zsh</code>\ninstead of <code>bash</code>.</p></li></ul>\n<p>Bottom line: don&#39;t use <code>wordexp(3)</code> on untrusted inputs, even with <code>WRDE_NOCMD</code>.</p>\n<ol><li>Previously, macOS&#39;s <code>wordexp(3)</code> would shell out to Perl. <code>wordexp-helper</code> appears to be the &quot;modern&quot; solution. <a href=\"#fnref-perl\">↩</a></li></ol>","headings":[]}}