Yesterday I introduced Galactic Outreach, my cold email tool for people who sell to local businesses. Today: the feature I shipped last night, and the part of it that actually took the time.
The feature
Getting your logo and brand colours into an email template is dull work. You already put all of that on your website, so now the template editor can go and get it.
Click the 🌐 in the editor, type yourcompany.com, hit Collect, and you get back:
- your company name (you can use it as the From name)
- up to 12 logos and pictures
- up to 8 colours (the one your site declares for itself comes first)
- your email, phone, address and social profiles
Nothing gets applied on its own. You click what you want, one thing at a time. Pictures are copied into the template instead of linked, so they don’t break if you redesign your site. They have to
be PNG, JPEG or GIF and 2 MB at most, because most mail clients don’t show SVG or WebP.
The scary part
“Type any URL and my server will fetch it” is the textbook setup for SSRF (server-side request forgery). My app runs on a cloud VM, so one typed URL could reach:
-
169.254.169.254, the cloud provider’s metadata service 😬 -
127.0.0.1:3000, the app itself with nothing in front of it - the other containers on its private network
So every request goes through a safeFetch() that checks it first:
httporhttpson ports 80/443 only.-
Resolve the name, and every address it points to must be public. If even one record is private, it refuses. That includes IPv4 hidden inside IPv6, like
::ffff:10.0.0.1. - Pin the address it just checked. Otherwise DNS could answer differently between the check and the connection (that’s DNS rebinding).
- Follow redirects by hand, 3 at most, running every check again on each one.
- Size caps and timeouts on everything.
Step 3 is the one people skip. With Node’s http you can override lookup so the socket can only connect to the address you already approved:
// simplified from safe-fetch.ts
const req = https.request(url, {
lookup: (_host, options, cb) => {
if (options.all) cb(null, [pinned]);
else cb(null, pinned.address, pinned.family);
},
timeout: 8000,
});
Enter fullscreen mode Exit fullscreen mode
I used node:http instead of fetch() because fetch() can’t pin an address without adding undici. I also didn’t add an HTML parser. Names, colours and contact details all live in meta tags,
JSON-LD and CSS, and simple pattern matching reads those fine.
Try it
Templates → open one → 🌐. How it works: galacticoutreach.com/docs/templates#from-your-website