The profile page
let customers add a website.

The rule was simple.

It had to start with https,
then a host name,
and nothing else.

The check was one line of Ruby.

url =~ /^https:\/\/[a-z0-9.-]+$/

Start of the string,
end of the string,
or so everyone read it.

In Ruby
^ and $ do not mean that.

They mean the start and end
of a line.

Any line.

So someone saved a website
with two lines in it.

javascript:alert(document.cookie)//

then a newline,

https://example.com

The second line matched.

The check passed.

The value went into an href
on every page
that showed the profile.

Browsers remove newlines
from URLs before they use them.

So the link became
javascript:alert(document.cookie)//https://example.com

The first part ran as script.

The two slashes
turned the rest into a comment.

Anyone who clicked
ran the attacker's code
inside their own session.

Ruby is not the only one.

In Python
$ also matches
just before a newline
at the very end.

re.match(r"^[a-z0-9_]+$", "admin\n")

That matches,
and the newline goes through
into a log line,
a header,
or a file name.

PHP behaves the same way
unless you add the D modifier.

The fix is a different anchor.

In Ruby,
\A for the start of the string,
\z for the end.

Lower case z.

The capital \Z
still forgives one trailing newline.

In Python,
use re.fullmatch,
which has to match everything.

Rails noticed this years ago.

Its format validator
refuses a pattern with ^ or $
unless you say multiline is what you meant.

The error tells you
to use \A and \z.

But that only guards the validator.

The same pattern
in a plain if statement
gets no warning at all.

So search your code
for regular expressions
used as gates.

Every one that starts with ^
and ends with $
is worth a second look.

Then feed it a test
that every check should fail.

A valid value,
a newline,
and something you do not want.

If that passes,
the pattern was only ever
reading one line of the story.

– Serguey Asael Shinder