Do 3 things today: redirect every plain-HTTP request to HTTPS, start a Strict-Transport-Security header on a short max-age you raise over weeks, and set Secure and HttpOnly on the session cookie. That covers most of man in the middle prevention on a managed host, whose TLS certificate already stops traffic being read or changed in transit.
Man in the middle prevention for a web app: the 8 settings, in priority order
Man-in-the-middle prevention on a hosted web app comes down to 8 settings in priority order: HTTPS with a permanent redirect, staged HSTS, Secure and HttpOnly session cookies, no mixed content, certificate verification left on for outbound calls with signed webhooks inbound, passkeys on owner and admin accounts, a CAA record with a locked registrar, and Subresource Integrity on third-party scripts.
These are the measures that counter a man-in-the-middle attack on an app you host, not on a network you run, so some familiar advice about Wi-Fi hardware and endpoint products stays out of the list. Knowing what a man-in-the-middle attack is helps with the why; the list below is the fix. Most of the eight are host and deploy settings, which puts them inside DevOps for startups, and each one prevents a MITM attack at a different point between the user and your server.
I order the rows for how to prevent a man-in-the-middle attack with the least work: rows 1 to 3 close the gaps any attacker on a shared network can use, rows 4 and 5 close the ones the app opens by itself, and rows 6 to 8 need a more capable attacker. The “where it is set” column matters as much as the order, because the hard part of defending against a man-in-the-middle attack on a managed host is knowing which settings are yours. The “how it can break the site” column is my own summary of each setting’s documented behavior.
| Setting | What it closes | Where it is set | How it can break the site | Owner page |
|---|---|---|---|---|
| 1. HTTPS on every hostname, with a permanent redirect from HTTP | Traffic read or changed on the wire | The host’s edge; Vercel answers plain HTTP with a 308 that can’t be disabled | A WebSocket client has to connect over HTTPS directly, because WSS does not support redirects | Adding HTTPS to a website |
| 2. HSTS, staged, before any preload decision | The plain-HTTP request on a return visit | A response header; Vercel sends max-age=63072000 per hostname by default | With includeSubDomains, a subdomain without HTTPS becomes unreachable for browsers that saw the header, until max-age runs out | The HSTS header and the other security headers |
3. Session cookie with Secure, HttpOnly, SameSite, and the __Host- prefix where the framework allows | A cookie sent over plain HTTP or read by a script | The auth library’s cookie options or the auth provider’s settings | SameSite=None without Secure is refused; a __Host- cookie with a Domain attribute is refused | Session and token controls |
4. No mixed content, with upgrade-insecure-requests as the net | Scripts and API calls fetched over HTTP from an HTTPS page | Asset and API URLs in the code; the Content-Security-Policy header | A blocked http:// script or fetch() call stops that feature on the live site | Apps that work locally but not in production |
| 5. Certificate verification on for outbound calls and the database; signatures checked on inbound webhooks | Your server talking to an impostor; a forged event | HTTP client options, the database connection string, the provider’s signing secret | A call to a server with a private certificate fails until its authority is added | Webhook security |
| 6. A passkey or security key on owner and admin accounts | A login relayed through a look-alike page | Each account’s security settings, in the app and on every provider dashboard | Losing the only key locks the owner out unless recovery codes exist | A second factor on owner and admin accounts |
| 7. A CAA record, a locked registrar with its own second factor, and a Certificate Transparency look | A certificate issued for your domain by someone else | The DNS host, the registrar account, a CT log search | A CAA record that leaves out the host’s authority blocks the host’s next certificate | Adding HTTPS to a website |
| 8. Subresource Integrity on any third-party script loaded from a CDN | A CDN file changed in transit or at the source | The integrity attribute on the script or link tag | A CDN file that changes without a new hash is refused | Production security headers |
Why it matters: HTTPS against a man in the middle, what the certificate already prevents and what it cannot
HTTPS with a valid certificate prevents 3 things: reading the traffic, changing it in transit without detection, and impersonating your domain to a browser. It does not cover the first plain-HTTP request, a cookie sent without Secure, an outbound call with verification switched off, a look-alike proxy login, or a certificate wrongly issued elsewhere.
Those three come straight from RFC 8446, the TLS 1.3 specification: the server side of the channel is always authenticated, data sent after the handshake is only visible to the endpoints, and it cannot be modified by attackers without detection. On a managed host you get them without asking. Vercel’s SSL documentation says Vercel will automatically try to generate a certificate for every domain once it is added to a project, which only works once the DNS records are added and propagated, and attempts renewal 14 to 30 days before the certificate expires. Its plan page lists automatic HTTPS and SSL as a Hobby plan feature. If Vercel is your host, its certificate and default headers are part of whether Vercel is safe for a production app. So against a man-in-the-middle attack, HTTPS does the largest share of the work on most hosted apps before you touch a setting, in my estimate.
Plain HTTP is now the exception rather than the rule. Google’s HTTPS transparency report shows 95 percent of pages loaded in Chrome on Windows came over HTTPS in the week of September 27, 2026, against 98 percent on Mac and 99 percent on Android, measured from Chrome users who share usage statistics. The gaps that remain are the five below, and each has its own setting.
| Threat on the path | HTTPS with a valid certificate | What closes the rest |
|---|---|---|
| Someone reading the traffic | Prevents it: data is only visible to the endpoints | Nothing more needed |
| Someone changing the traffic | Prevents it: changes are detected | Nothing more needed |
| A server pretending to be yours | Prevents it for a browser that checks certificates | Nothing more needed |
| The first plain-HTTP request, before the browser knows your site | Does not prevent it: the RFC calls that first visit vulnerable | Setting 2, HSTS, and the redirect |
A cookie set without Secure | Does not prevent it: the cookie can travel on a plain-HTTP request | Setting 3, cookie attributes |
| Your own server calling an API with verification switched off | Does not prevent it: the call is encrypted to whoever answers | Setting 5, verification left on |
| A user logging in through a look-alike proxy | Does not prevent it: both legs of the relay carry valid certificates | Setting 6, passkeys |
| A certificate wrongly issued for your domain by another authority | Does not prevent it: the browser trusts the certificate | Setting 7, CAA and Certificate Transparency |
One failure sits outside this table altogether. 10 of the 21 third-party apps trusted the client: the server accepted whatever the browser asserted. Those 21 were public and held-out apps built by other teams, which I audited between June and July 2026; they are a selected set, not a sample of all apps. Transport security does nothing for that failure, because the one sending the false value is the user at the far end of a perfectly valid connection.
How it works: each setting, what it closes, and how to set it without breaking the site
The five groups below follow the table’s order. Each names the dashboard or config file before any code, and the behavior described comes from the RFCs, MDN and each tool’s own documentation, not from tests I ran.
How to stop a man-in-the-middle attack on the first request: the redirect and staged HSTS
A man-in-the-middle attack on the first request is stopped by a permanent redirect plus HSTS, which makes a browser that has seen the header refuse plain HTTP. HSTS goes on in stages: confirm every subdomain serves HTTPS, then hold max-age at five minutes, a week and a month before a year. Preloading is a separate choice, slow to undo.
The short max-age in the three things to do today is where HSTS starts, and a year is where it ends. The redirect comes before it: a permanent redirect at the edge, a 301 or a 308, on the apex, on www and on every subdomain that serves anything. Vercel’s CDN forwards plain HTTP to HTTPS with a 308, and that redirect can’t be disabled. How to add HTTPS to a website on other hosts, with the certificate and the registrar settings, is its own job; the order is what matters here.
HSTS works because a browser that receives the header over HTTPS treats the host as HTTPS-only for max-age seconds, and ignores the header entirely when it arrives over plain HTTP. RFC 6797, the HSTS specification names the gap it leaves as the bootstrap vulnerability: when a user types or follows an http address to a site the browser does not yet know, that initial interaction “is vulnerable to various attacks”. The redirect plus a long-lived header shrinks the exposure to that one first visit.
The order across the whole domain:
- List every hostname and subdomain in DNS, internal ones included, and confirm each one answers on HTTPS. The HSTS preload list’s guidance puts this check before anything else.
- Redirect plain HTTP to HTTPS everywhere, permanently.
- Send the header with a max-age of 5 minutes. Add
includeSubDomainsonly when step 1’s list came back clean; until then, send it without. - Raise max-age to a week, then a month, checking for broken pages at each stage and waiting out the full max-age before the next.
- Go to a year or more (a year is also the preload list’s minimum), and treat preloading as a separate decision. The same site notes that many browsers already upgrade HTTP navigations to HTTPS on their own, says “While HSTS is recommended, HSTS preloading is not recommended”, and warns that a removal “takes months for a change to reach users with a Chrome update”.
The first-stage value, as the preload site writes it:
Strict-Transport-Security: max-age=300; includeSubDomains
Each host sets the HSTS header and the other production security headers in its own config file, so the syntax differs from host to host while the stages do not. Any honest answer to what is a rollback plan, and what it can undo lists HSTS as the exception: a redeploy removes the header, but browsers that already saw it keep enforcing it until max-age runs out.
On Vercel the header is already there. Vercel’s default response headers give strict-transport-security: max-age=63072000, two years, and Vercel’s encryption docs add that custom domains use HSTS “only for the particular subdomain”, while *.vercel.app also carries includeSubDomains and preload. So on Vercel the choice is not whether to send HSTS but whether to add includeSubDomains to a custom domain through a custom header in the project, and that addition is the step to stage.
Cookies, sessions and mixed content
A session cookie needs 3 attributes: Secure keeps it off plain HTTP, HttpOnly keeps it from scripts, and SameSite limits cross-site sending. Mixed content is the other gap: a plain-HTTP image or video gets upgraded, a plain-HTTP script or API call gets blocked, and the upgrade-insecure-requests directive is the net.
MDN’s Set-Cookie reference gives the exact rules: Secure sends the cookie only on https: requests (localhost excepted), HttpOnly forbids JavaScript from reading it, and SameSite=None must come with Secure. The __Host- prefix goes further. A cookie with that prefix must be set with Secure from an HTTPS page, with no Domain attribute and with Path=/, so it only ever goes back to the host that set it. You set all of these in the auth library’s cookie options or in the managed auth provider’s settings, not in a header you assemble yourself.
In one of my own apps, the secure cookie flag was one of five controls switched on only when NODE_ENV equals production, and the deploy never set it. The rest of that finding belongs to authentication best practices. So I read the Set-Cookie header on the live site rather than the code, since the code said the flag was there.
Mixed content is a page on HTTPS fetching something over HTTP. MDN on mixed content splits it in two: browsers upgrade image, video and audio requests to HTTPS and block insecure requests for all other resource types, which takes in scripts, stylesheets and fetch() calls. So one http:// script or API address is a security gap and a bug at once. The Content-Security-Policy directive upgrade-insecure-requests is the net, since it upgrades all requests to HTTPS, blockable mixed content included. When the symptom has already shown up, why an https page will not load an http API walks through it.
A cookie that does get stolen lives as long as its session, so expiry, refresh and revocation are the limit on the damage; those are session and token controls.
Server-to-server traffic: outbound calls, the database connection, and inbound webhooks
Server-to-server interception is prevented by leaving certificate verification on, and 4 lines switch it off: Node’s rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0, Python’s verify=False, curl’s -k, and a Postgres sslmode that does not verify the server. Inbound webhooks are proven by their signature, not by the address they came from.
This is the gap the app’s own code opens. Typically a call fails on a certificate error in development, the quick fix is to switch verification off, and the quick fix ships. Each of the four lines is documented by its own maker:
| The line that switches verification off | Stack | What its documentation says it does | What to write instead |
|---|---|---|---|
rejectUnauthorized: false, or NODE_TLS_REJECT_UNAUTHORIZED=0 in the environment | Node.js | Certificate validation is disabled; the variable “makes TLS, and HTTPS by extension, insecure” | Leave the default (true); for a private authority, pass its certificate in the ca option |
verify=False | Python Requests | Accepts any TLS certificate and ignores hostname mismatches and expired certificates | Leave verify at its default; for a private authority, pass the path to its CA bundle as verify |
-k or --insecure | curl in scripts | Skips the verification step and proceeds without checking | --cacert with the authority’s PEM file |
sslmode=require, prefer, allow or disable | Postgres (libpq) | require encrypts but gives no MITM protection; prefer is the default | sslmode=verify-full with the provider’s CA certificate |
The last column is what I do, drawn from the Node.js and Requests docs: leave the default on, and if the far end uses a private authority, add that authority rather than disabling the check. Node.js’s CLI documentation calls the environment variable strongly discouraged, Requests’ SSL verification docs say verify=False makes the application vulnerable to man-in-the-middle attacks, and curl’s manual warns that --insecure makes the transfer insecure. Your editor’s project-wide search finds each string one at a time; from the project root, one command finds all four:
grep -rnE "rejectUnauthorized:[[:space:]]*false|NODE_TLS_REJECT_UNAUTHORIZED|verify[[:space:]]*=[[:space:]]*False|curl.*[[:space:]](-[a-zA-Z]*k[a-zA-Z]*|--insecure)([[:space:]]|$)|sslmode=(disable|allow|prefer|require)" \
--exclude-dir=node_modules --exclude-dir=.git .
The database connection is the quiet one. PostgreSQL’s sslmode table gives require eavesdropping protection but no MITM protection, gives verify-full both, and makes prefer the default, which the same page says is not recommended in secure deployments. Supabase, as one managed provider, accepts connections without SSL unless “Enforce SSL on incoming connections” is turned on in Database Settings, and offers its CA certificate for download in the same place so that verify-full can work.
Traffic arrives as well as leaves. A webhook is a request from outside that claims to come from a provider, and the signature check is what proves the claim; the rest of webhook security builds on that one check. The code’s own path to production counts as traffic too: what merges into the deployed branch is controlled by review and required checks, which is the job of GitHub branch protection.
The proxy login page: what stops an attacker who relays your real site
A proxy login page defeats HTTPS and one-time codes, because both legs of the relay are validly encrypted and the code is relayed too. A passkey or security key stops it: the credential is scoped to the site that registered it and will not sign for a look-alike domain. Short sessions and re-authentication before sensitive changes limit the rest.
HTTPS cannot help here. As I understand the mechanism, the user lands on a look-alike domain with its own valid certificate, the page relays your real login, and a one-time code typed there is relayed as well, so the attacker ends up holding the session cookie. MITRE ATT&CK’s Adversary-in-the-Middle entry covers this family, and among its procedure examples lists relays that sit “between a legitimate website and a phished user to capture all transmitted data including usernames, passwords, authentication tokens, and session cookies and tokens”.
What stops it is a credential bound to the real site. Under the W3C WebAuthn specification, a public key credential “can only be accessed by origins belonging to that Relying Party”, and the browser and the authenticator enforce that together. A passkey registered on your domain has nothing to offer the look-alike.
What I’d use to limit the damage when a session is taken anyway: short-lived sessions, a fresh sign-in before anyone changes the account email, payout details or API keys, and a notice when a new device signs in. Start with two-factor authentication for owner and admin accounts, in the app and on the hosting, database, registrar and payment accounts, because those logins reach everything else.
Certificates you did not ask for: CAA, Certificate Transparency, and whether to pin
Certificate pinning is the wrong fix for a web app: Chrome removed HTTP public key pinning over very low adoption and the risks of denial of service and hostile pinning. A wrongly issued certificate is handled by a CAA record naming the authorities allowed to issue, the host’s own included, and a Certificate Transparency search for unrequested certificates.
A CAA record lets the domain holder name the authorities allowed to issue certificates for the domain, and a compliant authority must check for it before issuing, as RFC 8659, the CAA record sets out. The record must include the authority your host issues from. Vercel uses Let’s Encrypt, and Vercel’s note on CAA records says a CAA record that doesn’t permit Let’s Encrypt blocks issuance.
Certificate Transparency logs let domain owners see which authorities have issued which certificates, when, and for which domains, so you can look for ones you never requested. The record syntax and the search itself belong with the HTTPS setup work, not here. I’d rate a taken-over registrar or DNS account as a more realistic DNS threat to a small SaaS than a rogue authority, so the control that matters most is a second factor and a transfer lock on that account.
For third-party scripts, Subresource Integrity is the matching control. MDN’s Subresource Integrity page describes the integrity attribute as a cryptographic hash the fetched file must match, and says the browser refuses to load a file that does not.
HTTP public key pinning was the old answer to rogue certificates. MDN lists it as obsolete, and Chrome’s removal notes for Chrome 72 grant that it “provides security against certificate misissuance” before removing it for the two reasons above. So do not pin on the web. In a mobile app, pin only with backup pins and a tested rotation plan; I’d tell most small teams not to pin at all.
How to remove a man-in-the-middle attack: what an app owner does after an interception
Removing a man-in-the-middle attack means ending what the attacker took, in 6 steps: end the sessions, rotate the credentials that crossed the path, close the missing setting, review and undo account changes made in the window, decide who must be told, and write the timeline. Nothing is removed from the server itself.
There is nothing to delete, because the attacker was on the path, not in the app. Removal means taking back what they took and closing the gap they used, and these are the steps to fix a MITM attack from the app’s side:
- 01 Sign out every session for the affected accounts, or for every user if you are unsure, knowing that on token-based auth an access token already issued can stay valid until it expires
- 02 Rotate what may have crossed the path: passwords for the affected users, and any API keys and tokens that were sent in the clear
- 03 Close the gap: find which of the eight settings was missing and set it
- 04 Review what the sessions did in the window, such as email, payout, API key and role changes, and undo them
- 05 Decide who must be told, whether users, customers or a regulator; that is a legal question, and nothing here is legal advice
- 06 Write down the timeline: when it started, what was exposed, what you changed and when
Step one has a catch on token-based auth. Supabase’s sign-out docs, as one example, say access tokens of revoked sessions remain valid until their expiry time, so a user signed out everywhere stays signed in until that token runs out. Shorter access-token lifetimes shrink that window, which is part of session and token controls. Step two is slowest when one key sits in several services, so plan how to rotate API keys safely before you start revoking.
When the interception happened on one user’s own network or device, the app-side steps are the first and the fourth. On the user’s side, the first step is to leave that network. The second is to check the device for a root certificate or a proxy setting nobody meant to install.
These steps end what you can see. They do not prove there was nothing else, and if the review in step four turns up changes nobody can explain, treat it as a wider compromise and start from the first 30 minutes after an app is hacked.
How to check your own app: six inside checks
Prevention is proven from inside the app with 6 checks: every hostname answers on HTTPS, the live site’s console shows no mixed content, the code has none of the four verification-off lines, the database connection verifies the server, an altered webhook is rejected, and owner and admin accounts give nothing away without the second factor.
The outside-in checks (the redirect, the HSTS header and preload status, the cookie flags, an external TLS scan, the Certificate Transparency search) belong with detecting the attack from the server’s side, pass criteria included, so they are not repeated here. Each check below passes on a correct build and fails on a broken one.
- 01 Hostnames: list every name in DNS and request each one over HTTPS before
includeSubDomainsgoes on. Pass: every name returns the site or a redirect over HTTPS. Fail: any name that errors or times out. Keep: the list, dated - 02 Mixed content: open the deployed production site over HTTPS, never the local dev server (which usually runs on plain HTTP and cannot show mixed content), in desktop Chrome or Firefox with the developer console open, and click through sign-up, login and the core journeys. Pass: no mixed-content warning. Fail: any warning that a request was upgraded or blocked. Keep: a screenshot
- 03 Verification-off lines: run the search command from the server-to-server section, then look for
NODE_TLS_REJECT_UNAUTHORIZEDby name in the host's environment variable list. Pass: no hit outside test fixtures. Fail: any hit in code or config that ships. Keep: the search output - 04 Database: read the connection string's
sslmode, or the driver's own TLS option. Pass:verify-full, trusting the CA certificate your provider publishes. Fail:require,preferor no setting at all. Where the provider documents no CA, record that and the mode in use. Keep: the string with the password removed - 05 Webhooks: in a test environment, send a real test event from the provider and see it accepted, then send the same body with one field changed and the original signature header. Pass: a rejection status and no change in the app's data. Fail: the altered event is accepted. Keep: both responses
- 06 Second factor: sign in to each owner and admin account with the password alone, in the app and on the host, database, registrar and payment dashboards. Pass: no admin page, admin data or dashboard setting is reachable until the second factor is passed. Fail: anything is. Keep: dated screenshots
Two notes on reading those results. On Vercel, Secret environment variables are write-only after saving (older Sensitive ones now work as Secrets), so the third check reads the variable’s name, not its value. On Supabase, a password sign-in yields a session at assurance level aal1, and the docs leave enforcement to custom rules you write in the frontend, backend and database, so the refusal in the sixth check has to come from your app or its policies.
Two deliverables of the Production Hardening Sprint have checks that cover this ground. Deliverable 7.13 is verified this way: pass an external TLS scan at the top grade and file a DNS record export in the runbooks. Deliverable 3.4 is verified this way: inspect response headers and exercise the application to confirm intended protections without broken flows.
Where the sprint fits
No sprint deliverable is named man-in-the-middle prevention. The six closest to these settings, by their titles on the scope, are Domain, DNS and TLS hygiene (7.13), Production security headers (3.4), Session and token controls (1.2), Webhook signature verification (5.1), Two-factor authentication for owner and admin accounts (1.11) and Targeted external penetration testing (3.10). Deliverable 7.13 is the closest to this page: enforce HTTPS everywhere with HSTS and a CAA record, lock the registrar, document the DNS records, and confirm renewal ownership. Formal third-party certifications and independent audit opinions are separate from these engineering deliverables. Each item and how it is verified is on the published scope.
Common questions about stopping interception
How to protect yourself from MITM attacks?
Keep devices updated, never click through a certificate warning, use a passkey wherever a site offers one, and treat login links in email with suspicion; that is what I ask of a founder’s own team. For admin work away from the office, a phone hotspot is a safer path than Wi-Fi you do not know. A VPN moves where you place your trust, but it does nothing against a look-alike login page.
Are man-in-the-middle attacks common?
Less common on the network than they used to be, and the proxy-login kind is the one I’d plan for; no public count is one I would trust. Google’s transparency report shows 95 to 99 percent of Chrome page loads over HTTPS on Windows, Mac and Android in the week of September 27, 2026, which leaves a listener on the network little in plain text. MITRE’s entry for the technique, last modified in May 2026, lists phishing pages that work as a relay among its procedure examples.
Are MITM attacks active or passive?
Both. Reading the traffic is passive, and changing or relaying it is active. Encryption answers the passive kind, because data sent after the handshake is only visible to the endpoints; integrity and server authentication answer the active kind, because changes are detected and an impostor server cannot pass as yours.
What are 5 ways to prevent cyber attacks?
For a small web app, I’d pick these five: a second factor (MFA) on every owner and admin account, updates applied to the framework and its dependencies, authorization checked on the server for every request, secrets kept out of the browser and the repository, and backups you have restored at least once.
If you have a working app built with these tools and need it ready for real customers, this is what we do.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase