<?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>Go | FirewallSync</title><link>https://firewallsync.com/tags/go/</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/go/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></channel></rss>