
Sendy Is Insecure: How Not to Implement ReCAPTCHA - matteocontrini
https://victorzhou.com/blog/sendy-recaptcha-security/
======
benhomie
I am the author of Sendy.

There are a lot of miscommunication between me and the author of this post.
The selected snippets of messages posted on the article seem to put me in a
bad light, however it's only one side of the story. The selected messages
posted on the article are ones that are favorable to his argument. For example
the author did not post a screenshot of me saying that I will be looking into
this but went ahead to write an elaborate blog post immediately to put Sendy
in an unfavourable light.

Bugs and issues with security has been and always will be the top priority
with Sendy over the years. I agree the client side parameter 'subform'
bypasses the reCAPTCHA and should be fixed. It is an oversight. And it will be
fixed.

~~~
tmikaeld
Unrelated to the issue:

I've been asking this hundreds of times but never got an answer, why doesn't
Sendy add a visual e-mail builder?

Customers are going crazy over the issues with Wysiwyg and tables to make
simple two-column designs, while drag/drop builders are straightforward and
simple while keeping the code fully compatible with mailers.

Basically all competitors have drag/drop builders now, even the simpler one-
time-pay scripts on Envato.

~~~
Gys
check out [https://mjml.io/](https://mjml.io/). Its a great solution for
creating email layouts in code. I use it with vcode where it has an option to
preview the result.

------
dylz
Sendy has some massive problems with code quality and always had. The last
time I took a look at it (because I needed to send emails), it was quite
horrendous. I would not be surprised if it was insecure - was literally a
"nooooooope" right back out.

Background: I write PHP for fun, I don't compete with Sendy, I don't have any
stake in any service that competes with Sendy. I also don't expect PHP to be
written in the most OOP-everything Symfony-esque way either. But this is a bit
too much for me even.

It doesn't follow any modern standards or guidelines; it doesn't use any
templating system, just PHP hardcoded inline with HTML in hundreds of files
(no modern framework, lightweight or not), concatenated variables into SQL
queries and HTML. The author seems to hate using { } for if/else statements,
which has the potential of introducing fun bugs if not extremely careful. 1
and 2 letter variable names are common (and I don't mean in the way of $i or
$j); parts of it border on insane or bizarre; there are 500 character lines
where instead of an error generation feature they just die() with a full HTML
document as a string for error pages; functions are written inline all over
the place and they all use the keyword global; there are no parameterised
queries - it's covered in repetitive mysqli_real_escape_string and query
concatenation. There is a massive amount of copy-pasted code: instead of some
kind of single database management layer, there's a re-definition of a
function that connects to a database with the same name in dozens of files,
all of which try to read global variables (doesn't take any parameters)

~~~
Tomte
That's all true (I haven't checked myself, but lots of people talk about it on
Reddit and elsewhere).

Still, the big selling point is: Inexpensive. Easy to install (simple PHP).
Looks relatively polished.

All the "competitors" are either much more expensive, or you need to be an
enthusiast in the tech stack they have chosen.

~~~
knadh
Shameless plug: [https://listmonk.app](https://listmonk.app) (FOSS. Written in
Go)

~~~
nickjj
I'm curious, what made you go with a multiple lists approach instead of
tagging users on a single list?

A lot of popular (but expensive) services like ConvertKit have moved to a
tagging approach many many years ago.

It's so much easier to manage your list of emails when it's a single list and
then you add tags to specific users like "purchased X". This way the email
address only exists in 1 spot and you can segment on the tags.

The Sendy approach (and from the looks of it your app too) becomes very
unwieldy with having to manage, parse and merge multiple lists on a regular
basis.

~~~
knadh
listmonk supports arbitrary nested tags and attributes in the form of JSON. A
subscriber can have properties like {"purchased_x": true, ...}. It's possible
to issue complex SQL expressions to filter and send campaigns to subscribers.

The multi-list approach has several other benefits. When manager / sender (and
other) permissions get introduced, it will be straight forward to restrict
users to managing certain lists. In addition, multiple lists allow subscribers
to selectively subscribe / unsubscribe from lists.

Internally, the structure is simple. There is only one subscribers table and
subscriber data is not duplicated anywhere. List (foreign key) relationships
are in a separate table.

~~~
nickjj
That sounds very promising.

So in theory could you have 1 list and each subscriber has many tags, and then
you can segment on those tags?

Also, do you have any public success stories beyond your own Zerodha
campaigns? Have you compared delivery / bounce / etc. rates vs Sendy and other
tools using the same email providers? Also how fast can you send emails out
through SES?

------
mtlynch
Nice writeup, @vzhou842.

I feel like part of the problem here is just miscommunication. Victor and Ben
seem to be talking past each other.

This line stood out to me:

>>There’s no way to implement Google’s reCAPTCHA in an API.

>That can’t be right - the reCAPTCHA documentation has a dedicated section on
Server Side Validation!

I assume what Ben meant was that it's impossible to implement reCAPTCHA
_entirely_ in an API. The nature of reCAPTCHA requires you to have a UI
element, which would be impossible in an API. Victor interpreted the claim as
if Ben said that an API can't _support_ reCAPTCHA, which would be incorrect,
but may not be what Ben meant.

I feel like the lead sort of got buried in Victor's report. The headline to me
is that _abusers are actively automating phony signups with this
vulnerability_. I don't see that stated explicitly anywhere. The closest is
this line in Victor's third email, after the conversation has gotten somewhat
heated:

>What good is reCAPTCHA if anybody with a computer can write a script in 5
minutes to spam your email list with thousands of fake signups.

The fact that attackers are exploiting this in the wild seems to be the most
salient point, but I'm not sure that Ben knew that from the correspondence
shown.

~~~
benhomie
> I assume what Ben meant was that it's impossible to implement reCAPTCHA
> entirely in an API. The nature of reCAPTCHA requires you to have a UI
> element, which would be impossible in an API.

That’s exactly what I meant. Thanks for picking up on this.

> The fact that attackers are exploiting this in the wild seems to be the most
> salient point, but I'm not sure that Ben knew that from the correspondence
> shown.

I know that and hence was working on a fix for the next update.

Even though some of my comments may not be in agreement with the author, but I
did mentioned in my email conversation that I am looking into it. But of
course that was being left out of the post, no screenshots of that comment was
found in the author’s post.

If I had released the next update without addressing this issue then yes feel
free to write a post with these accusations. But I wasn’t given the benefit of
the doubt.

------
Tomte
> Sendy treats these email addresses as distinct, but they’re actually largely
> duplicates because of the Gmail period trick.

But they are distinct! Why should anyone have to special-case some provider's
hack? Do you expect Sendy to special-case plus addresses? For which hosts?

Sure, it's nice if Sendy handles special cases, but it's strictly a bonus
feature, and not doing it is also correct.

(The main issue with the hidden field is a valid complaint, of course, and it
really doesn't look good)

~~~
WillPostForFood
He didn't say they should have a special case. He just made a factual
statement, "Sendy treats these email addresses as distinct, but they’re
actually largely duplicates because of the Gmail period trick," and looked to
implement recaptcha as a solution, which was poorly implemented. However, if
you were going to do create one special case for a provider, gmail would be a
good choice.

~~~
megous
These addresses are distinct. They just point to the same mailbox. At best
they can be called aliases. /p

------
dmix
Wow, the response where Ben claims bots aren’t humans so they won’t remove
HTML form elements before submitting is an embarrassing look for Sendy.

------
slig
For anyone wanting an alternative, there's Mailwizz - one time payment, and
you can self host and connect it with SES/Sparkpost/Mailgun/etc.

I've mentioned it at least a dozen times here on HN because it's an underrated
piece of software that works and is actively maintained, unfortunately their
marketing is very weak.

Bonus: the code is not obfuscated, it's built on PHP using a proper framework
and the author is very active on the support forums. I'm hosting it on
Webfaction (currently migrating to Opalstack) for cheap and almost no
maintenance.

~~~
tmikaeld
Thanks for this, haven't seen it even though I've been actively searching!

I think it's hard to get taken seriously by companies when you sell a "script"
on Invato too and I'm not sure why they do this? Why not just sell it yourself
and receive more of the profits?

~~~
slig
I guess it's somewhat easier if the product is good enough (Envato features
it) and the developer cares more about developing than marketing it.

~~~
tmikaeld
I guess that's true, everyone coming to the site will then see it featured and
buy it.

Nothing prevents him from doing it in the future though, when he already have
enough customers to make a recurring cost profitable.

------
cphoover
That would make me concerned about the engineering culture at the company. The
initial ill-reasoned response sounded like a response he gathered from the
tech team. If they knew what they were doing they would agree with Victor.

~~~
stephenr
Afaik sendy is a one man band playing a triangle.

------
smw
Obviously, this should be fixed, but shouldn't you also be doing double-opt-in
and making the user confirm the subscription by clicking a link in an email?

~~~
vzhou842
Definitely, and I was doing that, but everytime a new spam variant (with gmail
period trick) of an email gets subscribed that same email gets ANOTHER double-
opt-in email from me. This is my guess as to why I was getting reported as
spam so much - people would literally be getting spammed by me
(unintentionally).

EDIT: forgot to mention I'm the author of this post

------
mrskitch
Yeah we definitely use recaptcha in a GraphQL API for sign-ups to
browserless.io. There is a doc on how to do so programmatically via JS in the
browser. The docs aren’t great, per se, but it is definitely doable.

EDIT: docs are here:
[https://developers.google.com/recaptcha/docs/display#javascr...](https://developers.google.com/recaptcha/docs/display#javascript_api)
though the lifecycle of how this all works is confusing. I’ll have to do a
write-up on this at some point.

------
nickjj
Does anyone know of a self hosted email list management solution that uses a
tagging based approach of managing users instead of multiple lists, supports
auto responders and also has a decent API for at least adding users to a list?

I have not found any self hosted solutions (paid or free / open source) and
I've looked a lot in the past. I've seen services like Drip and ConvertKit
offer this but both of them are at the high end of hosting costs. To put
things into perspective it's $80 / month for a list of 3,000 users with
ConvertKit even if you send nothing.

One of the more polished tools I've seen is Freecodecamp's
[https://github.com/freeCodeCamp/mail-for-
good](https://github.com/freeCodeCamp/mail-for-good) project but it doesn't
support auto responders or tagging. I've opened issues on this 2+ years ago at
[https://github.com/freeCodeCamp/mail-for-
good/issues/196](https://github.com/freeCodeCamp/mail-for-good/issues/196) and
[https://github.com/freeCodeCamp/mail-for-
good/issues/197](https://github.com/freeCodeCamp/mail-for-good/issues/197) but
I don't think they are going to implement it.

~~~
pixelbash
I could be interested in writing one, having just written a semi complex
wishlist management app with some overlap in user management features. Would
nodejs suit a project like this do you think?

~~~
nickjj
I'm more of a Flask, Phoenix and Rails type of guy but if you prefer using
Node sure, you probably couldn't go too wrong if that's what you have
extensive experience in.

~~~
pixelbash
Also a Rails guy but have found Node is quite a good fit for async work like
managing email queues. Would use Phoenix but don't have enough production
experience in it yet..

------
jonathanbull
I run a service in the same space ([https://emailoctopus.com/amazon-
ses](https://emailoctopus.com/amazon-ses)) and we see a lot of Sendy
defectors. Their code desperately needs a rewrite.

------
benhomie
An update of Sendy had just been released to address the reCAPTCHA issue →
[https://sendy.co/get-updated](https://sendy.co/get-updated)

------
hnarn
Just reading the first two responses from "Ben" in that e-mail thread and
parsing his attitude should make it obvious to anyone that this is a project
you should steer well clear of.

------
Thorrez
>Plus, it should be super easy for you to fix this - all you have to do is
remove the check for "subform" and drop support for that field in your API.

It might not be that easy, because there might be a bunch of users currently
depending on the current behavior, and as soon as a real ReCAPTCHA token is
required, they will break. They might need to introduce a 3rd ReCAPTCHA
option. So they would have 3 options "ReCAPTCHA off", "ReCAPTCHA legacy weak",
and "ReCAPTCHA on".

------
vzhou842
Author of the post here - Happy to answer any questions. I'll be editing the
post with responses/updates from Sendy. Hopefully this convinces them to
release a patch!

~~~
new_guy
Why would you willingly use such a garbage POS script? There's better services
out there, or you could just roll your own.

~~~
ridaj
Any suggestions?

~~~
jonathanbull
Seeing as you asked, I’ll give my startup a shameless plug:

[https://emailoctopus.com/amazon-ses](https://emailoctopus.com/amazon-ses)

------
amirhirsch
Here is a patch for subscribe.php (4.0.3.2) to address the captcha issue from
this post and another issue that allows bypassing double-opt-in by setting
silent=true: [https://pastebin.com/dT1NszTt](https://pastebin.com/dT1NszTt)

this change requires verifying secret api key in the subform=no case and
restricts opt_in bypass to this subscribe api usage (since captcha is not good
enough to stop all bots)

------
desheikh
I've seen cases where spammers abuse sign-up forms to generate spam (by using
the name field for their content). Without server side validation it would be
trivial for a malicious user to accomplish this.

Shameless plug - I run a hosted alternative to Sendy
[https://www.mailblast.io/](https://www.mailblast.io/)

------
parliament32
>// DO NOT USE - it's illegal and will get you in trouble.

>WARNING: DO NOT use this code to attack a real email list without permission.
That's super illegal and can get you in serious trouble.

"Super illegal"?

~~~
dylz
almost certainly construed as some kind of CFAA violation

------
homero
So are you recreating the recaptcha code in your custom form?

------
amirhirsch
Author seems self-righteous and does not understand what he is being told by
Ben from Sendy. This is additionally evidenced by hundreds of lines of email
and a blog post for what could be addressed with 12 lines of code
modification.

Sendy is open source (albeit not free) and running on your own server. Ben's
response says because you are using a customized webform instead of sendy's
provided forms, that you need to handle the captcha code on the form and
siteverify on the backend on your own.

Before sendy had recaptcha support you would have to modify the subscribe code
to include a call to captcha siteverify. Here's some PHP code for how you call
siteverify before proceeding with something: [https://www.adam-
bray.com/2018/04/02/adding-recaptcha-with-p...](https://www.adam-
bray.com/2018/04/02/adding-recaptcha-with-php/)

You can avoid running your own backend altogether now anyway since SES
supports bulk template emails. You can store all your form submissions using a
lambda/cloudfunction/webworker to verify the captcha and store the list. When
you want to send email you can pull your list into something running on your
laptop and then invoke the SES bulk templated email from there. You can use
lambdas for pixels and custom links that update records for those users. I
wrote my own angular-firebase version of this

~~~
Jwarder
My reading of the issue is that Sendy's webform allows the external requester
to bypass the server-side captcha logic by changing a client-side "hidden"
input. If you want to be protected then you have to customize the form.

~~~
amirhirsch
I have the source code too and checked it already. The gist of the code is:

if subform:

    
    
       if captcha fails:
    
           feedback = "Failed recaptcha test"

... if feedback!='Failed recaptcha test' (&& other stuff)

    
    
       do subscribe
    
    

edit: misread the code and formatting on HN didn't even show my intent, but
the subform check doesn't contain the subscribe logic. The bug is clearly that
it doesn't check if the captcha has passed.

~~~
lilyball
And the point is that anyone with even a modicum of dev experience can remove
the `subform` field and automate submission to the otherwise-standard form and
completely bypass ReCAPTCHA.

~~~
amirhirsch
The issue goes even deeper: if subform is set to no then sendy considers the
user as added via api. This _should_ mean that it would verify_api_key before
allowing such a submission, but sendy doesn't verify the API key for subscribe
calls (doh!). Old forum posts suggest that double-opt-in is a solution,
however not only can you bypass the captcha and form with subform=no, you can
also bypass double-opt-in via the subscribe API by sending silent=true in your
POST.

