~/blog/static-site-with-serverless-functionsPOSTED
by Shady Nathan Tawfik · July 4, 2026 · 4 min

Static Site, Serverless Functions, Twelve Findings

A site builder platform with a content delivery network in front and functions behind it. The static layer was clean; the function layer and the mail configuration were not.

Interactive[blog-2.0]

TL;DR: The static front end was clean; the serverless function layer behind it was not. I tested origin handling, rate limiting and the mail chain, and I explain why platform defaults drift.

The scenario

I ran this as a black-box review: no credentials, only the publicly visible surface. Here is what I saw before I touched anything.

A marketing and lead generation site built on a hosted site builder, served through a content delivery network with serverless functions for form handling. Scope covered the edge configuration, function exposure, form handling, mail authentication and administrative surface.

I removed all identifying information. The target is described only by sector and technology class so the pattern can be reused.

The pattern

The pattern I recognised — and that I keep finding in similar estates:

The site builder market produces a specific architecture: pre-rendered pages from a visual editor, a content delivery network in front, and a small number of serverless functions for the parts that cannot be static. Contact forms, coupon validation and authentication all live in that last group. It is a reasonable design and it moves almost all the attack surface out of the server you can inspect, which is precisely the problem.

Static pages are genuinely hard to attack. There is no interpreter on the request path, so the injection families have nothing to bind to. The functions are where the estate becomes interesting again, and they are usually the least documented part of the deployment, because they were added to make one form work and were never treated as an application. Each function is a small program with its own environment, its own secrets and its own input, deployed from a pipeline nobody reviews.

The other recurring theme is that platform-managed sites inherit defaults nobody chose. Mail authentication, security headers and subdomain policy are configured once during onboarding and then drift. What looks like a hardened static site is often a hardened front end sitting on an unexamined function layer.

TL;DR: A site builder platform with a content delivery network in front and functions behind it. The static layer was clean; the function layer and the mail configuration were not.

Findings

SeverityFindingEvidence
HOCHFunction endpoint callable without origin validationAccepts cross-origin form posts by design
MITTELNo rate limit on the form functionUnlimited submissions to a mail-sending endpoint
MITTELMail authentication absent for the sending domainSpoofable outbound mail
MEDIUMPlatform preview environment indexedDraft content publicly reachable
LOWFramework banner on function responsesRuntime and platform version identifiable
LOWSubdomain policy permissiveWildcard records for asset hosts

The attack path

Visitor formContent delivery networkServerless functionEnvironment secretsMail sending APICross-origin callerNo origin check, no rate limit

Reproduction in my lab

Every command below targets a lab container I control. Nothing here is aimed at a live system.

Test your own function endpoint for origin and rate handling

~/code/bash bash
# target: your own deployed function
$ curl -s -o /dev/null -w '%{http_code}\n' 'https://example.org/api/contact'

# does it care where the request came from
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  -H 'Origin: https://evil.example' -H 'Content-Type: application/json' \
  -d '{"email":"a@b.example"}' 'https://example.org/api/contact'

# and is it rate limited
$ for i in $(seq 1 12); do
    curl -so /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
      -d "{\"n\":$i}" 'https://example.org/api/contact'
  done; echo

Detection in your own estate

Inventory functions behind a static front end

~/code/bash bash
$ curl -s https://example.org/ | grep -oiE '(href|src|action)="[^"]+"' \
  | grep -iE 'api|form|submit|action|endpoint' | sort -u

# the builder's own form endpoint is often the same one
$ curl -s https://example.org/ | grep -oiE 'data-[a-z-]*(endpoint|form|action)="[^"]+"' | sort -u

Verify the mail authentication chain for the sending domain

~/code/bash bash
$ dig +short TXT example.org | grep -i spf
$ dig +short TXT _dmarc.example.org
$ dig +short TXT selector1._domainkey.example.org | head

Remediation

Validate origin and rate-limit at the edge, before the function runs

~/code/nginx nginx
# rate limit the form path, keyed on client
limit_req_zone $binary_remote_addr zone=forms:10m rate=5r/m;

location = /api/contact {
    limit_req zone=forms burst=3 nodelay;
    limit_req_status 429;

    if ($http_origin !~* '^https://(www\.)?example\.org$') {
        return 403;
    }
    proxy_pass http://functions;
}

Set an explicit, enforced mail authentication policy

~/code/text text
# SPF: authorise exactly the sending services, then hard fail
v=spf1 include:<sending-provider> -all

# DMARC: start at monitoring, move to quarantine, then reject
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.org; fo=1
# after two clean reporting cycles:
# v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.org
# after four:
# v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.org

Once an attacker gains access, they can perform the following actions:

Function endpoint callable without origin validation
No rate limit on the form function
Mail authentication absent for the sending domain
Platform preview environment indexed

Takeaways

  • A static front end does not mean a static estate.
  • Functions added to make a form work deserve a real code review.
  • Rate limits belong at the edge, where the CDN already sees every request.
  • Platform defaults drift; mail policy and subdomain policy need an owner.

For educational purposes only. Every reproducible command targets my own lab environment (localhost), never a live system. © 2026