<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Input Validation | FirewallSync</title><link>https://firewallsync.com/tags/input-validation/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Sun, 01 Nov 2026 04:40:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/input-validation/index.xml" rel="self" type="application/rss+xml"/><item><title>Escape User Input in Regex and SQL LIKE: Tested Fixes</title><link>https://firewallsync.com/posts/escape-user-input-regex-sql-like/</link><guid>https://firewallsync.com/posts/escape-user-input-regex-sql-like/</guid><pubDate>Sun, 01 Nov 2026 04:40:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>A search box that drops user text into new RegExp() or a LIKE pattern is a small injection bug: a dot matches anything, .* matches everything, and a % turns a filtered search into a full table scan that returns every row. Parameterised queries do not fix it. This guide shows the correct escaping function in Python, JavaScript and Go, the LIKE ESCAPE rules for PostgreSQL and SQLite, and the replacement-string trap that catches people after they have escaped the pattern.</description><enclosure url="https://firewallsync.com/posts/escape-user-input-regex-sql-like/cover_hu10145603437681109998.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/escape-user-input-regex-sql-like/cover_hu10145603437681109998.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/escape-user-input-regex-sql-like/cover_hu10145603437681109998.jpg" alt="Dozens of wooden letterpress type blocks, letters and numerals in warm brown tones, scattered on a wooden surface" width="1200" height="630"></p><p>Search features have a habit of taking a user&rsquo;s text and dropping it straight into a pattern: <code>new RegExp(query)</code>, <code>re.search(term, line)</code>, <code>WHERE name LIKE '%' || $1 || '%'</code>. The user meant &ldquo;find this text&rdquo;. The engine reads &ldquo;match this pattern&rdquo;. A dot becomes &ldquo;any character&rdquo;, <code>.*</code> becomes &ldquo;everything&rdquo;, and <code>%</code> in a LIKE search returns every row you were filtering.</p>
<p><strong>The short answer:</strong> when user text should match literally, escape it with the platform function (<code>re.escape</code>, <code>RegExp.escape</code>, <code>regexp.QuoteMeta</code>) before building the pattern. For SQL <code>LIKE</code>, bind the value as a parameter <strong>and</strong> escape <code>\</code>, <code>%</code> and <code>_</code> in it, with an explicit <code>ESCAPE '\'</code>. If the text is also used as a replacement string, pass a function, not a string.</p>
<p>This article is about user text that should be treated as literal. If your product lets users write real regular expressions, escaping is the wrong tool; see <a href="/posts/user-supplied-regex-redos-safe-engines/">User-Supplied Regex: How to Run It Without ReDoS</a>.</p>
<h2 id="what-goes-wrong-without-escaping">What goes wrong without escaping</h2>
<p>We ran each example below and show the output. Test environment: Python 3.13.18, Node.js 24.22.2 (V8 13.6) and 22.23.4, Go 1.24.7, SQLite 3.53.1 (Python&rsquo;s <code>sqlite3</code> module) and PostgreSQL 16.15, all on Linux.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># Python 3.13: search log lines for a user-supplied term</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span>logs <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;user=alice&#34;</span>, <span style="color:#e6db74">&#34;user=al.ce&#34;</span>, <span style="color:#e6db74">&#34;user=bob&#34;</span>, <span style="color:#e6db74">&#34;user=admin&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>q <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;al.ce&#34;</span>
</span></span><span style="display:flex;"><span>[l <span style="color:#66d9ef">for</span> l <span style="color:#f92672">in</span> logs <span style="color:#66d9ef">if</span> re<span style="color:#f92672">.</span>search(q, l)]             <span style="color:#75715e"># [&#39;user=alice&#39;, &#39;user=al.ce&#39;]</span>
</span></span><span style="display:flex;"><span>[l <span style="color:#66d9ef">for</span> l <span style="color:#f92672">in</span> logs <span style="color:#66d9ef">if</span> re<span style="color:#f92672">.</span>search(re<span style="color:#f92672">.</span>escape(q), l)]  <span style="color:#75715e"># [&#39;user=al.ce&#39;]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>q <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;.*&#34;</span>
</span></span><span style="display:flex;"><span>all(re<span style="color:#f92672">.</span>search(q, l) <span style="color:#66d9ef">for</span> l <span style="color:#f92672">in</span> logs)               <span style="color:#75715e"># True: matches every line</span>
</span></span><span style="display:flex;"><span>[l <span style="color:#66d9ef">for</span> l <span style="color:#f92672">in</span> logs <span style="color:#66d9ef">if</span> re<span style="color:#f92672">.</span>search(re<span style="color:#f92672">.</span>escape(q), l)]  <span style="color:#75715e"># []</span>
</span></span></code></pre></div><p>The consequences depend on where the pattern is used. In a search box it is a wrong result. In a filter that decides which records a user may see, a match against tenant or account names, or a log-redaction rule, a wildcard where you expected a literal is an authorisation or data-exposure bug. Unescaped input also lets the user add nested quantifiers to your pattern, which is a route to ReDoS in backtracking engines.</p>
<h2 id="regex-use-the-platform-escaper">Regex: use the platform escaper</h2>
<h3 id="python-reescape">Python: <code>re.escape</code></h3>
<p>The <a href="https://docs.python.org/3/library/re.html#re.escape">Python docs</a> describe <code>re.escape</code> as escaping &ldquo;special characters in <em>pattern</em>&rdquo;, useful &ldquo;if you want to match an arbitrary literal string that may have regular expression metacharacters in it.&rdquo; Since Python 3.7, &ldquo;only characters that can have special meaning in a regular expression are escaped.&rdquo;</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>re<span style="color:#f92672">.</span>escape(<span style="color:#e6db74">&#34;a.b*c(d)&#34;</span>)   <span style="color:#75715e"># &#39;a\\.b\\*c\\(d\\)&#39;</span>
</span></span><span style="display:flex;"><span>pattern <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>compile(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;\b&#34;</span> <span style="color:#f92672">+</span> re<span style="color:#f92672">.</span>escape(user_term) <span style="color:#f92672">+</span> <span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;\b&#34;</span>, re<span style="color:#f92672">.</span>IGNORECASE)
</span></span></code></pre></div><h3 id="go-regexpquotemeta">Go: <code>regexp.QuoteMeta</code></h3>
<p>Go&rsquo;s <a href="https://pkg.go.dev/regexp#QuoteMeta"><code>QuoteMeta</code></a> &ldquo;returns a string that escapes all regular expression metacharacters inside the argument text; the returned string is a regular expression matching the literal text.&rdquo;</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Go 1.24.7
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">regexp</span>.<span style="color:#a6e22e">QuoteMeta</span>(<span style="color:#e6db74">&#34;al.ce*(x)&#34;</span>))                                  <span style="color:#75715e">// al\.ce\*\(x\)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">regexp</span>.<span style="color:#a6e22e">MustCompile</span>(<span style="color:#a6e22e">regexp</span>.<span style="color:#a6e22e">QuoteMeta</span>(<span style="color:#e6db74">&#34;al.ce&#34;</span>)).<span style="color:#a6e22e">MatchString</span>(<span style="color:#e6db74">&#34;alice&#34;</span>)) <span style="color:#75715e">// false
</span></span></span></code></pre></div><h3 id="javascript-regexpescape-and-why-the-old-snippet-is-not-enough">JavaScript: <code>RegExp.escape</code>, and why the old snippet is not enough</h3>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape">MDN</a> describes <code>RegExp.escape()</code> as returning &ldquo;a new string that can be safely used as a literal pattern for the <code>RegExp()</code> constructor&rdquo;, and marks it Baseline 2025. It is present in Node.js 24 and absent in Node.js 22:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>node v24.22.2 RegExp.escape: function
</span></span><span style="display:flex;"><span>node v22.23.4 RegExp.escape: undefined
</span></span></code></pre></div><p>The output looks odd at first: <code>RegExp.escape(&quot;foo.bar&quot;)</code> returns <code>\x66oo\.bar</code>. MDN explains that a leading digit or ASCII letter is written as a <code>\x</code> escape so that, if the result is &ldquo;immediately preceded by <code>\1</code>, <code>\x0</code>, <code>\u000</code>, etc., the leading character doesn&rsquo;t get interpreted as part of the escape sequence.&rdquo; Other punctuators such as <code>-</code> are also written as <code>\x</code> escapes.</p>
<p>That is exactly where the widely copied replace-based escaper falls short. We compared the two in Node.js 24:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// Node.js 24.22.2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">escapeRegExp</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">s</span> =&gt; <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">/[.*+?^${}()|[\]\\]/g</span>, <span style="color:#e6db74">&#34;\\$&amp;&#34;</span>); <span style="color:#75715e">// common fallback
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 1. User text placed right after a backreference
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;(a)\\1&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">escapeRegExp</span>(<span style="color:#e6db74">&#34;1&#34;</span>)).<span style="color:#a6e22e">source</span>;   <span style="color:#75715e">// &#39;(a)\\11&#39;  -&gt; test(&#34;aa1&#34;) is false
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;(a)\\1&#34;</span> <span style="color:#f92672">+</span> RegExp.<span style="color:#a6e22e">escape</span>(<span style="color:#e6db74">&#34;1&#34;</span>)).<span style="color:#a6e22e">source</span>;  <span style="color:#75715e">// &#39;(a)\\1\\x31&#39; -&gt; test(&#34;aa1&#34;) is true
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 2. User text placed inside a character class
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;[&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">escapeRegExp</span>(<span style="color:#e6db74">&#34;a-z&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;]&#34;</span>).<span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#34;m&#34;</span>);   <span style="color:#75715e">// true: became the range a-z
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;[&#34;</span> <span style="color:#f92672">+</span> RegExp.<span style="color:#a6e22e">escape</span>(<span style="color:#e6db74">&#34;a-z&#34;</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;]&#34;</span>).<span style="color:#a6e22e">test</span>(<span style="color:#e6db74">&#34;m&#34;</span>);  <span style="color:#75715e">// false: matches only a, -, z
</span></span></span></code></pre></div><p>In both cases the fallback produced a valid pattern that matched something different from what was intended. MDN&rsquo;s advice is direct: &ldquo;don&rsquo;t try to re-implement its functionality,&rdquo; because <code>RegExp.escape()</code> &ldquo;is designed to use escape sequences that work in many more edge cases/contexts than hand-crafted code is likely to achieve.&rdquo;</p>
<p>For code that must also run on older runtimes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// Prefer the built-in; the fallback is only safe when the escaped text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// is not placed inside [...] or directly after a backslash escape.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">escapeRegExp</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">typeof</span> RegExp.<span style="color:#a6e22e">escape</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;function&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">?</span> RegExp.<span style="color:#a6e22e">escape</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">:</span> <span style="color:#a6e22e">s</span> =&gt; <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">/[.*+?^${}()|[\]\\]/g</span>, <span style="color:#e6db74">&#34;\\$&amp;&#34;</span>);
</span></span></code></pre></div><h2 id="escaping-does-not-cover-the-replacement-string">Escaping does not cover the replacement string</h2>
<p>Teams that escape the pattern often forget that the replacement argument has its own syntax.</p>
<p><strong>JavaScript.</strong> In <code>String.prototype.replace</code>, a replacement <em>string</em> interprets <code>$&amp;</code>, <code>$1</code>, <code>$&lt;name&gt;</code> and similar. A function&rsquo;s return value is used as is:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// Node.js 24.22.2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;$&amp;$&amp;&#34;</span>;                               <span style="color:#75715e">// user-controlled
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;Hello NAME&#34;</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">&#34;NAME&#34;</span>, <span style="color:#a6e22e">name</span>);                <span style="color:#75715e">// &#39;Hello NAMENAME&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#e6db74">&#34;Hello NAME&#34;</span>.<span style="color:#a6e22e">replace</span>(<span style="color:#e6db74">&#34;NAME&#34;</span>, () =&gt; <span style="color:#a6e22e">name</span>);          <span style="color:#75715e">// &#39;Hello $&amp;$&amp;&#39;
</span></span></span></code></pre></div><p><strong>Python.</strong> The <code>re.escape</code> docs say it &ldquo;must not be used for the replacement string in <code>sub()</code> and <code>subn()</code>, only backslashes should be escaped.&rdquo; In practice, passing a function avoids the problem entirely:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>s <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;name: NAME&#34;</span>
</span></span><span style="display:flex;"><span>re<span style="color:#f92672">.</span>sub(<span style="color:#e6db74">&#34;NAME&#34;</span>, <span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;C:\new\1&#34;</span>, s)          <span style="color:#75715e"># re.error: invalid group reference 1 at position 7</span>
</span></span><span style="display:flex;"><span>re<span style="color:#f92672">.</span>sub(<span style="color:#e6db74">&#34;NAME&#34;</span>, <span style="color:#66d9ef">lambda</span> m: <span style="color:#e6db74">r</span><span style="color:#e6db74">&#34;C:\new\1&#34;</span>, s)  <span style="color:#75715e"># &#39;name: C:\\new\\1&#39;</span>
</span></span></code></pre></div><p>A replacement string that happens to contain <code>\n</code> or <code>\1</code> either changes the output or raises, depending on the text. Either is a bug when the text comes from a user. The same caution applies to anything that renders model or user output into templates; see <a href="/posts/llm-improper-output-handling-treat-model-output-as-untrusted-input/">treating LLM output as untrusted input</a>.</p>
<h2 id="sql-like-parameters-are-not-enough">SQL LIKE: parameters are not enough</h2>
<p>A bind parameter keeps the value out of the SQL grammar, so <code>'; DROP TABLE</code> is just text. But <code>LIKE</code> interprets <code>%</code> (any sequence) and <code>_</code> (any single character) inside the value, bound or not. The <a href="https://www.postgresql.org/docs/current/functions-matching.html">PostgreSQL docs</a> state: &ldquo;The default escape character is the backslash but a different one can be selected by using the ESCAPE clause.&rdquo; Writing <code>ESCAPE ''</code> &ldquo;effectively disables the escape mechanism.&rdquo;</p>
<p>Escape the escape character first, then the wildcards:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">like_escape</span>(s: str, esc: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">&#34;</span>) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> s<span style="color:#f92672">.</span>replace(esc, esc <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span>)<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;%&#34;</span>, esc <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;%&#34;</span>)<span style="color:#f92672">.</span>replace(<span style="color:#e6db74">&#34;_&#34;</span>, esc <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;_&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># SQLite: no escape character unless you declare one</span>
</span></span><span style="display:flex;"><span>cur<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;SELECT name FROM u WHERE name LIKE ? ESCAPE &#39;</span><span style="color:#ae81ff">\\</span><span style="color:#e6db74">&#39;&#34;</span>,
</span></span><span style="display:flex;"><span>    (<span style="color:#e6db74">&#34;%&#34;</span> <span style="color:#f92672">+</span> like_escape(term) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;%&#34;</span>,),
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Test table: <code>alice</code>, <code>bob</code>, <code>50% off</code>, <code>a_b</code>, <code>axb</code>. Results with the search value bound as a parameter:</p>
<table>
  <thead>
      <tr>
          <th>Search term</th>
          <th>SQLite, bound, not escaped</th>
          <th>SQLite, escaped + <code>ESCAPE '\'</code></th>
          <th>SQLite, escaped, no <code>ESCAPE</code></th>
          <th>PostgreSQL, escaped (default <code>\</code>)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>%</code></td>
          <td>all 5 rows</td>
          <td><code>50% off</code></td>
          <td>none</td>
          <td><code>50% off</code></td>
      </tr>
      <tr>
          <td><code>_</code></td>
          <td>all 5 rows</td>
          <td><code>a_b</code></td>
          <td>none</td>
          <td>not run</td>
      </tr>
      <tr>
          <td><code>a_b</code></td>
          <td><code>a_b</code>, <code>axb</code></td>
          <td><code>a_b</code></td>
          <td>none</td>
          <td><code>a_b</code></td>
      </tr>
  </tbody>
</table>
<p>Two things stand out. First, a bound <code>%</code> or <code>_</code> returns every row: that turns a filtered search into an export of the table. Second, the escaping and the <code>ESCAPE</code> clause must agree. SQLite has no default escape character, so escaped input without <code>ESCAPE '\'</code> matched nothing. PostgreSQL uses backslash by default, but stating <code>ESCAPE '\'</code> anyway makes the query portable and removes the dependency on the default.</p>
<p>Two more differences we observed: SQLite&rsquo;s <code>LIKE</code> was case-insensitive for ASCII (<code>'ALICE' LIKE 'alice'</code> returned 1), while PostgreSQL&rsquo;s was case-sensitive (<code>false</code>; use <code>ILIKE</code> there). If your ORM offers a &ldquo;contains&rdquo; or &ldquo;starts with&rdquo; helper, check whether it escapes wildcards for your database before relying on it.</p>
<p>The same applies to other pattern operators: PostgreSQL&rsquo;s docs warn, &ldquo;Be wary of accepting regular-expression search patterns from hostile sources&rdquo;, for <code>SIMILAR TO</code> and POSIX regex operators.</p>
<h2 id="checklist">Checklist</h2>
<ul>
<li>Decide whether the user is supplying <strong>text</strong> or a <strong>pattern</strong>. Text gets escaped; patterns need a safe engine and limits.</li>
<li>Use <code>re.escape</code>, <code>RegExp.escape</code> or <code>regexp.QuoteMeta</code>. Feature-detect <code>RegExp.escape</code> on Node.js 22 and older browsers.</li>
<li>Never put fallback-escaped text inside <code>[...]</code> or directly after a backslash escape.</li>
<li>For <code>LIKE</code>: bind the value, escape <code>\</code>, <code>%</code>, <code>_</code> in that order, and always write <code>ESCAPE '\'</code>.</li>
<li>Use a function, not a string, when user text is the replacement in <code>replace</code> or <code>re.sub</code>.</li>
<li>Add a test with <code>.</code>, <code>.*</code>, <code>%</code>, <code>_</code>, <code>$&amp;</code> and a backslash in the input. The bug is invisible with ordinary names.</li>
</ul>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>We tested the runtimes and versions listed above; MySQL, SQL Server and Oracle have their own LIKE escape rules and were not tested. Other regex dialects (Java, .NET, PCRE) have their own quoting functions not covered here. The JavaScript fallback is shown to illustrate its limits, not as a recommendation.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>re.escape</code> purpose, 3.7 change, replacement-string warning</td>
          <td><a href="https://docs.python.org/3/library/re.html#re.escape">Python <code>re</code> docs</a> (cpython main, Doc/library/re.rst)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>QuoteMeta</code> behaviour</td>
          <td><a href="https://pkg.go.dev/regexp#QuoteMeta">Go <code>regexp</code> package</a> (src/regexp/regexp.go)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td><code>RegExp.escape</code> behaviour, leading-character rule, Baseline 2025, re-implementation warning</td>
          <td><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape">MDN RegExp.escape()</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>PostgreSQL default escape character, <code>ESCAPE ''</code>, regex caution</td>
          <td><a href="https://www.postgresql.org/docs/current/functions-matching.html">PostgreSQL pattern matching docs</a> (read from the 17.1 text)</td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Fallback escaper failing after <code>\1</code> and inside <code>[...]</code>; replacement-string behaviour; LIKE results; case sensitivity</td>
          <td>Our tests on the versions listed</td>
          <td>Verified (tested)</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>User-Supplied Regex: How to Run It Without ReDoS</title><link>https://firewallsync.com/posts/user-supplied-regex-redos-safe-engines/</link><guid>https://firewallsync.com/posts/user-supplied-regex-redos-safe-engines/</guid><pubDate>Sat, 31 Oct 2026 04:50:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>Search filters, alert rules, routing configs and log tools all end up accepting regular expressions from users. In a backtracking engine that is a denial-of-service switch you hand to whoever can type a pattern. This guide shows which engines are built for untrusted patterns (RE2, Go, Rust), which ones say plainly that they are not (.NET), what timeouts and possessive quantifiers can and cannot do, and two tested patterns for Python and Node.js.</description><enclosure url="https://firewallsync.com/posts/user-supplied-regex-redos-safe-engines/cover_hu13911530470990273703.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/user-supplied-regex-redos-safe-engines/cover_hu13911530470990273703.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/user-supplied-regex-redos-safe-engines/cover_hu13911530470990273703.jpg" alt="Taut strands of red, orange, pink, purple, yellow and white thread crossing at angles to form a dense grid against a blurred grey background" width="1200" height="630"></p><p>If your product lets users type a regular expression (a log search box, an alerting rule, a URL routing rule, a &ldquo;block messages matching&rdquo; filter), you have handed them a way to burn your CPU. In a backtracking engine, a short pattern and a short input can keep a thread busy for seconds or years. The fix is not a better regex review process, because you are not the one writing the patterns.</p>
<p><strong>The short answer:</strong> run user-supplied patterns on an engine that guarantees linear-time matching (RE2, Go&rsquo;s <code>regexp</code>, Rust&rsquo;s <code>regex</code>), cap pattern length, and reject features those engines do not support instead of quietly falling back. If you are stuck with a backtracking engine, run the match somewhere you can kill it on a deadline.</p>
<h2 id="why-a-backtracking-engine-is-the-problem">Why a backtracking engine is the problem</h2>
<p>Python&rsquo;s <code>re</code>, JavaScript&rsquo;s <code>RegExp</code> (V8&rsquo;s Irregexp) and .NET&rsquo;s default engine are backtracking engines. When a match fails, they back up and try every other way the pattern could have split the input. For most patterns that is fast. For patterns with nested or overlapping repetition, the number of ways to split the input grows exponentially.</p>
<p>OWASP&rsquo;s wiki page on <a href="https://wiki.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">ReDoS</a> describes an &ldquo;evil&rdquo; regex as one with grouping with repetition where the repeated group contains repetition or overlapping alternation. Its examples include <code>(a+)+</code>, <code>([a-zA-Z]+)*</code>, <code>(a|aa)+</code> and <code>(a|a?)+</code>.</p>
<p>We timed the first one in Python 3.13.18 against a string of <code>a</code> characters followed by <code>!</code>, so the match has to fail:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> re<span style="color:#f92672">,</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pat <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>compile(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;^(a+)+$&#39;</span>)
</span></span><span style="display:flex;"><span>pos <span style="color:#f92672">=</span> re<span style="color:#f92672">.</span>compile(<span style="color:#e6db74">r</span><span style="color:#e6db74">&#39;^(?:a++)+$&#39;</span>)   <span style="color:#75715e"># possessive quantifier, Python 3.11+</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> n <span style="color:#f92672">in</span> (<span style="color:#ae81ff">18</span>, <span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">22</span>, <span style="color:#ae81ff">24</span>):
</span></span><span style="display:flex;"><span>    s <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;a&#39;</span> <span style="color:#f92672">*</span> n <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39;!&#39;</span>
</span></span><span style="display:flex;"><span>    t <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter(); pat<span style="color:#f92672">.</span><span style="color:#66d9ef">match</span>(s); t1 <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter() <span style="color:#f92672">-</span> t
</span></span><span style="display:flex;"><span>    t <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter(); pos<span style="color:#f92672">.</span><span style="color:#66d9ef">match</span>(s); t2 <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter() <span style="color:#f92672">-</span> t
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;n=</span><span style="color:#e6db74">{</span>n<span style="color:#e6db74">:</span><span style="color:#e6db74">2d</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> backtracking=</span><span style="color:#e6db74">{</span>t1<span style="color:#e6db74">:</span><span style="color:#e6db74">8.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">s possessive=</span><span style="color:#e6db74">{</span>t2<span style="color:#e6db74">:</span><span style="color:#e6db74">.6f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">s&#34;</span>)
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>n=18 backtracking=  0.0088s possessive=0.000005s
</span></span><span style="display:flex;"><span>n=20 backtracking=  0.0429s possessive=0.000005s
</span></span><span style="display:flex;"><span>n=22 backtracking=  0.1680s possessive=0.000003s
</span></span><span style="display:flex;"><span>n=24 backtracking=  0.5356s possessive=0.000005s
</span></span></code></pre></div><p>Each additional two characters roughly quadrupled the time. Extrapolate to a 40-character input and one request ties up a worker for hours. Node.js 22 behaves the same way: <code>/^(a+)+$/.test('a'.repeat(24) + '!')</code> took 495 ms in our sandbox.</p>
<p>The possessive rewrite fixes this pattern because <code>a++</code> refuses to give characters back. That is a valid fix when <strong>you</strong> write the pattern. It does nothing when a user writes it, because the user will not write the possessive version.</p>
<h2 id="two-different-threats">Two different threats</h2>
<p>It helps to separate the cases, because the defences differ.</p>
<table>
  <thead>
      <tr>
          <th>Situation</th>
          <th>Who controls the pattern</th>
          <th>Main fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Your validator runs on user input</td>
          <td>You</td>
          <td>Review the pattern, use possessive quantifiers or atomic groups, limit input length</td>
      </tr>
      <tr>
          <td>Users submit patterns (search, rules, filters)</td>
          <td>The attacker</td>
          <td>Use a linear-time engine, cap pattern size, isolate matching</td>
      </tr>
      <tr>
          <td>User text is spliced into your pattern</td>
          <td>Partly the attacker</td>
          <td>Escape it (<code>re.escape</code>, or an escape helper in JavaScript) or use a plain substring search</td>
      </tr>
  </tbody>
</table>
<p>This article is about the second row. The third row is the most common way the second row happens by accident: someone builds <code>new RegExp(userInput)</code> to implement a &ldquo;contains&rdquo; search that never needed a regex.</p>
<h2 id="engines-that-are-built-for-untrusted-patterns">Engines that are built for untrusted patterns</h2>
<p><strong>RE2.</strong> Google&rsquo;s <a href="https://github.com/google/re2">RE2 README</a> states the design goal directly: it was built &ldquo;to handle regular expressions from untrusted users without risk&rdquo;, and &ldquo;one of its primary guarantees is that the match time is linear in the length of the input string&rdquo;. It also says its parser, compiler and execution engines &ldquo;limit their memory usage by working within a configurable budget&rdquo;. The price is features: RE2 &ldquo;does not support constructs for which only backtracking solutions are known to exist&rdquo;. That means no backreferences and no look-around.</p>
<p><strong>Go&rsquo;s <code>regexp</code>.</strong> The package uses RE2 syntax, and its source documents that the implementation &ldquo;is guaranteed to run in time linear in the size of the input&rdquo;.</p>
<p><strong>Rust&rsquo;s <code>regex</code> crate.</strong> Its <a href="https://github.com/rust-lang/regex">README</a> says &ldquo;all regex searches in this crate have worst case <code>O(m * n)</code> time complexity, where <code>m</code> is proportional to the size of the regex and <code>n</code> is proportional to the size of the string being searched&rdquo;. Its documentation says the crate &ldquo;also, to a certain extent, supports untrusted patterns&rdquo;, mainly &ldquo;by limiting their size by default&rdquo;, which you can tune with <code>RegexBuilder::size_limit</code>.</p>
<p>Note the <code>m</code> in that complexity. Linear in the input is not the same as free: a huge pattern still costs more to compile and run. That is why a pattern length cap belongs in front of every engine, safe or not.</p>
<h2 id="engines-that-say-they-are-not">Engines that say they are not</h2>
<p><strong>.NET.</strong> Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-regex">regex best-practices page</a> is unusually direct:</p>
<blockquote>
<p>The .NET regular expression engine does not offer protection against untrusted <em>patterns</em>, and applications should not create regular expression patterns from untrusted user-provided values.</p>
</blockquote>
<p>It adds that time-out values and <code>RegexOptions.NonBacktracking</code> (introduced in .NET 7, which &ldquo;guarantees linear-time processing in the length of the input&rdquo;) &ldquo;help protect against accidental performance problems in developer-authored patterns. They are not intended as a security boundary against malicious patterns.&rdquo; Use them for your own patterns, and do not treat them as permission to accept user-written ones.</p>
<p><strong>Ruby.</strong> Ruby 3.2 added two ReDoS mitigations, described in the <a href="https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/">3.2.0 release notes</a>. A memoization-based matcher completes &ldquo;most Regexp matching (about 90% in our experiments)&rdquo; in linear time, at the cost of memory &ldquo;proportional to the input length&rdquo;. Because that optimization &ldquo;cannot be applied to some kind of regular expressions, such as those including advanced features (e.g., back-references or look-around)&rdquo;, Ruby also added <code>Regexp.timeout</code>, a global setting, and a per-regex <code>timeout:</code> keyword on <code>Regexp.new</code>. Both help. Neither turns Ruby&rsquo;s engine into one designed for hostile patterns.</p>
<p><strong>Python and JavaScript.</strong> Python&rsquo;s <code>re</code> documentation describes no match timeout. Python 3.11 added possessive quantifiers and atomic groups <code>(?&gt;...)</code>, which help you write safer patterns but do nothing about patterns you did not write. JavaScript&rsquo;s <code>RegExp</code> has no timeout either. V8 has shipped an <a href="https://v8.dev/blog/non-backtracking-regexp">experimental non-backtracking engine</a> since V8 8.8, behind <code>--enable-experimental-regexp-engine</code>, which adds a non-standard <code>l</code> (&ldquo;linear&rdquo;) flag. V8&rsquo;s own post calls it experimental &ldquo;because Irregexp is orders of magnitude faster than the new engine on most common patterns&rdquo;. With the flag on, <code>/^(a+)+$/l</code> failed on a 50,000-character evil input in about 3 ms in our Node.js 22.23 test. Running production on an experimental V8 flag is a decision to make deliberately, not a default.</p>
<p>Accepting patterns also exposes you to bugs in the regex parser and compiler themselves, not just slow matching. Our write-up of <a href="/posts/gitlab-cve-2026-97229-ci-regex-rce-patch-and-hunt/">GitLab CVE-2026-97229</a> covers a remote code execution flaw in how GitLab compiled regular expressions from CI/CD configuration. Using a small, heavily fuzzed engine reduces that risk too.</p>
<h2 id="pattern-1-python-with-re2">Pattern 1: Python with RE2</h2>
<p>This uses the <code>google-re2</code> package (we tested 1.1.20251105 with Python 3.13). It caps pattern length, compiles with RE2, and turns unsupported syntax into a clear error your API can return as a 400.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> re2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>MAX_PATTERN <span style="color:#f92672">=</span> <span style="color:#ae81ff">256</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">compile_user_pattern</span>(pattern: str):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Compile a user-supplied pattern with RE2 or raise ValueError.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(pattern) <span style="color:#f92672">&gt;</span> MAX_PATTERN:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">&#34;pattern too long&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> re2<span style="color:#f92672">.</span>compile(pattern)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> re2<span style="color:#f92672">.</span>error <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ValueError</span>(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;unsupported or invalid pattern: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>) <span style="color:#f92672">from</span> None
</span></span></code></pre></div><p>What happened when we ran it:</p>
<table>
  <thead>
      <tr>
          <th>Input</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>^(a+)+$</code> searched against 1,000,000 <code>a</code> characters plus <code>!</code></td>
          <td>No match, returned in about 2 ms</td>
      </tr>
      <tr>
          <td><code>(\w+)\s\1</code> (backreference)</td>
          <td>Rejected: <code>invalid escape sequence: \1</code></td>
      </tr>
      <tr>
          <td><code>^(?=.*\d).+$</code> (look-ahead)</td>
          <td>Rejected: <code>invalid perl operator: (?=</code></td>
      </tr>
      <tr>
          <td>300-character pattern</td>
          <td>Rejected by the length cap</td>
      </tr>
  </tbody>
</table>
<p>The same evil pattern that took half a second on 24 characters in <code>re</code> finished a million characters in milliseconds. Two practical notes from the test: RE2 logs parse errors to stderr through its own logging, so expect noise in your logs when users submit bad patterns. And do not catch the error and retry with <code>re</code>. That fallback reopens the exact hole you closed.</p>
<h2 id="pattern-2-nodejs-with-a-killable-worker">Pattern 2: Node.js with a killable worker</h2>
<p>If you cannot add a native RE2 binding, isolate the backtracking engine. A worker thread can be terminated mid-match, which a <code>setTimeout</code> on the main thread cannot do: a regex blocks the event loop until it returns. This is tested on Node.js 22.23.4:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// safe_match.mjs: run an untrusted pattern in a worker and kill it after a deadline.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Worker</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;node:worker_threads&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">WORKER_SRC</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  const { parentPort, workerData } = require(&#39;node:worker_threads&#39;);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  const { pattern, flags, input } = workerData;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  try {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    parentPort.postMessage({ ok: true, match: new RegExp(pattern, flags).test(input) });
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  } catch (e) {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    parentPort.postMessage({ ok: false, error: e.message });
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">safeTest</span>(<span style="color:#a6e22e">pattern</span>, <span style="color:#a6e22e">input</span>, { <span style="color:#a6e22e">flags</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>, <span style="color:#a6e22e">timeoutMs</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">100</span>, <span style="color:#a6e22e">maxPattern</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">256</span> } <span style="color:#f92672">=</span> {}) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">pattern</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">maxPattern</span>) <span style="color:#66d9ef">return</span> Promise.<span style="color:#a6e22e">resolve</span>({ <span style="color:#a6e22e">ok</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;pattern too long&#39;</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#e6db74">/^[gimsuy]*$/</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">flags</span>)) <span style="color:#66d9ef">return</span> Promise.<span style="color:#a6e22e">resolve</span>({ <span style="color:#a6e22e">ok</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;bad flags&#39;</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> Promise((<span style="color:#a6e22e">resolve</span>) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">w</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Worker</span>(<span style="color:#a6e22e">WORKER_SRC</span>, {
</span></span><span style="display:flex;"><span>      eval<span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">workerData</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">pattern</span>, <span style="color:#a6e22e">flags</span>, <span style="color:#a6e22e">input</span> },
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">resourceLimits</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">maxOldGenerationSizeMb</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">32</span> },
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">timer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">setTimeout</span>(() =&gt; { <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">terminate</span>(); <span style="color:#a6e22e">resolve</span>({ <span style="color:#a6e22e">ok</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;timeout&#39;</span> }); }, <span style="color:#a6e22e">timeoutMs</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">once</span>(<span style="color:#e6db74">&#39;message&#39;</span>, (<span style="color:#a6e22e">m</span>) =&gt; { <span style="color:#a6e22e">clearTimeout</span>(<span style="color:#a6e22e">timer</span>); <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">terminate</span>(); <span style="color:#a6e22e">resolve</span>(<span style="color:#a6e22e">m</span>); });
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">once</span>(<span style="color:#e6db74">&#39;error&#39;</span>, (<span style="color:#a6e22e">e</span>) =&gt; { <span style="color:#a6e22e">clearTimeout</span>(<span style="color:#a6e22e">timer</span>); <span style="color:#a6e22e">resolve</span>({ <span style="color:#a6e22e">ok</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">false</span>, <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">message</span> }); });
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Results:</p>
<ul>
<li><code>safeTest('^(a+)+$', 'a'.repeat(40) + '!')</code> returned <code>{ ok: false, error: 'timeout' }</code> after 103 ms. On the main thread that match would not finish.</li>
<li><code>safeTest('^[a-z]+@example\\.com$', 'alice@example.com')</code> returned <code>{ ok: true, match: true }</code>.</li>
<li><code>safeTest('(unclosed', 'x')</code> returned the syntax error instead of throwing.</li>
</ul>
<p>The cost: spawning a worker per call averaged 25.4 ms in our sandbox, over 20 calls. That is fine for saving a rule. It is too slow for matching every log line. For high-volume matching, keep a small pool of long-lived workers and replace any worker you have to terminate, or move to an RE2 binding. This is a containment pattern, not a fix: an attacker can still make every match hit the timeout, so rate-limit pattern submissions and count timeouts per user.</p>
<h2 id="a-checklist-for-features-that-accept-patterns">A checklist for features that accept patterns</h2>
<ol>
<li><strong>Ask whether you need regex at all.</strong> Substring, prefix or glob matching covers most &ldquo;search&rdquo; features and has no ReDoS surface.</li>
<li><strong>Pick a linear-time engine</strong> (RE2 or a binding, Go <code>regexp</code>, Rust <code>regex</code>) for any pattern a user can write, including admins and API clients.</li>
<li><strong>Cap pattern length</strong> and, if you can, the number of patterns per user or rule set.</li>
<li><strong>Reject unsupported features with a clear message.</strong> Never fall back to a backtracking engine.</li>
<li><strong>Cap input length</strong> for each match. Linear time on a 2 GB input is still a lot of time. Our post on <a href="/posts/nodejs-decompression-bomb-zlib-limits/">decompression bombs in Node.js</a> covers the related problem of inputs that grow after you accept them.</li>
<li><strong>If you must backtrack, isolate it.</strong> Use a worker or process you can kill, a hard deadline, and a memory limit. Ruby&rsquo;s <code>Regexp.timeout</code> and .NET&rsquo;s <code>matchTimeout</code> are useful extra layers.</li>
<li><strong>Watch for it.</strong> Log match timeouts and compile failures by user, and alert on spikes.</li>
</ol>
<h2 id="limits-of-this-article">Limits of this article</h2>
<ul>
<li>Timings are from a shared cloud sandbox and will differ on your hardware. The growth rate, not the absolute numbers, is the point.</li>
<li>We did not test .NET or Ruby directly, and we do not cover Java, PCRE or other engines here. Statements about them come from their documentation, quoted above.</li>
<li>We tested one Python RE2 binding (<code>google-re2</code>). Node.js RE2 bindings exist, but we did not test one for this article.</li>
<li>A linear-time engine removes catastrophic backtracking. It does not remove the need for size limits, rate limits or patching the engine itself.</li>
</ul>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backtracking times for <code>^(a+)+$</code> in Python 3.13.18 and Node.js 22.23.4; RE2 and worker results</td>
          <td>Our tests, code above</td>
          <td>Verified (our sandbox)</td>
      </tr>
      <tr>
          <td>&ldquo;Evil&rdquo; regex conditions and examples; attacker can inject an evil regex</td>
          <td><a href="https://wiki.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">OWASP wiki: ReDoS</a></td>
          <td>Verified (archived wiki page)</td>
      </tr>
      <tr>
          <td>RE2: built for untrusted users, linear in input, memory budget, no backtracking-only constructs</td>
          <td><a href="https://github.com/google/re2">google/re2 README</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Go regexp: linear in the size of the input</td>
          <td><a href="https://github.com/golang/go/blob/master/src/regexp/regexp.go">Go source, regexp.go</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Rust regex: worst case O(m * n); untrusted patterns handled mainly by size limit</td>
          <td><a href="https://github.com/rust-lang/regex">rust-lang/regex README and docs</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>.NET: no protection against untrusted patterns; timeouts and NonBacktracking not a security boundary</td>
          <td><a href="https://learn.microsoft.com/en-us/dotnet/standard/base-types/best-practices-regex">Microsoft Learn: regex best practices</a></td>
          <td>Verified (docs source on GitHub)</td>
      </tr>
      <tr>
          <td>Ruby 3.2 memoization (about 90% linear) and Regexp.timeout</td>
          <td><a href="https://www.ruby-lang.org/en/news/2022/12/25/ruby-3-2-0-released/">Ruby 3.2.0 release notes</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Python 3.11 possessive quantifiers and atomic groups; no timeout documented</td>
          <td><a href="https://docs.python.org/3/library/re.html">Python re documentation</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>V8 experimental non-backtracking engine, <code>l</code> flag, slower than Irregexp</td>
          <td><a href="https://v8.dev/blog/non-backtracking-regexp">V8 blog</a></td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>