Skip to content

^ and $ match at every line

Level: 201 · anyone validating input with a regex

One line: In Ruby ^ and $ always match at line boundaries, so /^\d+$/ accepts "42\n<script>"; anchor a whole string with \A and \z. Perl and Python anchor the whole string by default, and Ruby's /m means what their /s and re.DOTALL mean.

In Perl and in Python, ^ and $ anchor the start and end of the string unless a multiline flag says otherwise. Ruby has no such mode: ^ and $ are line anchors, always, and the letter m was spent on something else. A pattern copied from either language changes meaning without changing a character.

Verified output of anchors_match_every_line_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

Validating an id:
  /^\d+$/      "42"             match
  /^\d+$/      "42\n<script>"   match
  /\A\d+\z/    "42\n<script>"   no match
  /\A\d+\z/    "42\n"           no match
  /\A\d+\Z/    "42\n"           match

Ruby's /m is Perl's /s: it lets . match a newline
  /a.b/        "a\nb"           no match
  /a.b/m       "a\nb"           match

Reading the output

  • /^\d+$/ matched "42\n<script>". ^ matched at the start, \d+ read 42, $ matched before the newline, and nothing required the rest of the string to match anything. A validation written this way — an id, a slug, a hostname allow-list — passes whatever an attacker puts on the second line.
  • \A and \z are the start and the very end of the string. \Z also allows one trailing newline, so "42\n" passes \Z and fails \z.
  • /m makes . match a newline. It does nothing to the anchors.

Three languages, one table

Every cell is taken from the three outputs on this page.

Ruby Perl Python
^ and $ by default every line the whole string the whole string
anchor every line (the default) /m re.MULTILINE
start, and very end, of the string \A\z \A\z \A\Z
end, or before one final newline \Z $ or \Z $
. matches a newline /m /s re.DOTALL

Note Python's \Z: it is Ruby's and Perl's \z, not their \Z.

What to do

Write \A and \z in every pattern that validates. The Rails security guide ↗ walks through this bypass in its section on regular expressions, and Rails' format validator makes you pass multiline: true before it accepts a pattern with ^ or $ in it — see validates_format_of.

If you are coming from another language

Perl — the same inputs:

Verified output of anchors_match_every_line_pl.pl — regenerated by tools/run_examples.py, never hand-typed.

Validating an id:
  /^\d+$/      "42"             match
  /^\d+$/      "42\n<script>"   no match
  /^\d+$/m     "42\n<script>"   match
  /^\d+$/      "42\n"           match
  /\A\d+\z/    "42\n"           no match
  /\A\d+\Z/    "42\n"           match

Perl's /m changes the anchors; /s lets . match a newline:
  /a.b/m       "a\nb"           no match
  /a.b/s       "a\nb"           match

Python — the same inputs, with re.fullmatch as a fourth way to say "the whole string":

Verified output of anchors_match_every_line_py.py — regenerated by tools/run_examples.py, never hand-typed.

Validating an id:
  ^\d+$                '42\n<script>'   no match
  ^\d+$ re.MULTILINE   '42\n<script>'   match
  ^\d+$                '42\n'           match
  \A\d+\Z              '42\n'           no match
  re.fullmatch(\d+)    '42\n'           no match

re.DOTALL lets . match a newline:
  a.b re.DOTALL        'a\nb'           match