^ 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+read42,$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.\Aand\zare the start and the very end of the string.\Zalso allows one trailing newline, so"42\n"passes\Zand fails\z./mmakes.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.