A serious vulnerability in cPanel was disclosed at the end of April — a pre-authentication CRLF injection in the login and session-loading endpoints, scored 9.8 on the CVSS scale, and very quickly added to the US government's Known Exploited Vulnerabilities catalogue. The "Sorry" ransomware group used it to compromise an estimated 44,000 cPanel installations in days.
We don't run cPanel. We've never run cPanel. Our control panel is software we wrote ourselves. So at first glance, this CVE was someone else's problem.
But "we don't use cPanel" is not the same as "we're not vulnerable." The bug isn't really a cPanel bug — cPanel just happens to be where it was found this time. The underlying mistake is generic. So we audited our own code for the same mistake. Here's what we found, and what we changed.
What CRLF injection actually is
A surprising amount of internet plumbing is built on text protocols where lines are separated by \r\n (carriage return + line feed). Headers in HTTP, headers in SMTP, the SIP protocol, IRC — anywhere a protocol sends headers, those headers are CRLF-delimited, and the body starts after the first blank line.
The bug class is: a program builds a header line by string-concatenating user input. The user supplies input containing \r\n. The protocol now sees an unintended new header — or worse, sees the end of headers and the start of an attacker-controlled body.
In cPanel's case, the injection was in HTTP-layer paths and gave the attacker arbitrary control over the response. The same bug class shows up everywhere headers are constructed by string concatenation.
What we audited
Our HTTP layer is actually fine — Go's net/http standard library refuses to send a header value containing \r or \n, so any HTTP-layer attempt at injection just fails with an error. We confirmed this with a code review and a quick fuzz, then moved on.
The SMTP layer was the part we suspected first. Our outbound mail code — for things like ticket replies, password reset emails, welcome emails to new customers — builds MIME messages by string-concatenating headers. There are about a dozen different places in our code where this happens. We audited all of them.
What we found:
- No envelope-recipient escalation was possible. Go's
net/smtpvalidatesRcptarguments, so an attacker couldn't smuggle a CC: into the SMTP envelope itself. That's a reassuring finding — the actual delivery routing was tamper-proof. - Header forgery in the message body was reachable. A user-supplied subject or display name could contain
\r\nand inject extra headers into the message we composed. That's not "delivers to extra recipients" — but it is "lets the attacker craft a message that looks like it came from us, in our envelope, with their content."
The most exposed entry point was our public contact form (the one with no auth on it at all, which any visitor to the website can fill in). It accepted email addresses that contained CRLF as long as the rest of the string also looked vaguely email-shaped.
What we changed
Replaced our email validator with net/mail.ParseAddress plus a 254-byte length cap. The previous validator was effectively contains("@") && contains("."), which lets through almost anything if you squint. ParseAddress is the canonical Go function for doing this, and it rejects CRLF cleanly.
Added a stripCRLF helper to every package that sends mail. It does what it says on the tin: removes \r and \n from a string. We apply it at every place where user-supplied data enters a header construction (subject lines, display names, free-text content that ends up in headers). Belt-and-braces. The validator should have already rejected the input, but we strip control characters before they hit the wire as a second layer.
Length caps on user-supplied header inputs. A 4MB subject line isn't an attack we'd identified, but it's also not something legitimate, so we cap it.
That was the patch. It took a working day to write, review, and roll out. We're rebuilding and re-signing the binaries now and they'll deploy after our standard sign-off.
The general lesson
The cPanel CVE is being talked about as a cPanel problem, but the vulnerability class long predates cPanel and will outlive it. Anywhere user input becomes part of a header in any text protocol, you have to filter the line terminator. Java, Go, Python, PHP, Rust — language doesn't matter; the protocol is the same.
If you build software, audit your own code the next time a CRLF injection CVE makes the news. The probability is low that you're vulnerable, but the cost of checking is one afternoon and the cost of missing one is your customers' data.
We watch the security industry's news because the bug types repeat even when the products don't. cPanel is not us. CRLF injection is everyone.