Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 5 – A04:2025 Cryptographic Failures

Transport security: TLS, HSTS and mixed content

What TLS protects and what it leaves open, settings that hold, HSTS for the first request, mixed content, and clients that never skip certificate checks.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain what TLS guarantees to one connection and what it leaves open
  • Choose TLS versions, cipher suites and certificate practices that meet ASVS 5.0.0 chapter 12
  • Check an HSTS header the way a browser reads it, and a domain against the preload rules
  • Find mixed content in a page and fix it at the source
  • Validate certificates in client code by trusting the right CA instead of switching verification off

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what TLS guarantees to one connection and what it leaves open
  • Choose TLS versions, cipher suites and certificate practices that meet ASVS 5.0.0 chapter 12
  • Check an HSTS header the way a browser reads it, and a domain against the preload rules
  • Find mixed content in a page and fix it at the source
  • Validate certificates in client code by trusting the right CA instead of switching verification off

Before you start

On this page

A shop moved to HTTPS years ago, and every page shows the padlock. On café Wi-Fi, three gaps remain: a new customer’s first visit still starts over plain HTTP, the checkout page loads one script from an http:// address, and a nightly job calls the payment API with certificate checks switched off. Each gap is a connection without TLS, or with its checks turned off: cleartext transmission (CWE-319, under A04:2025 Cryptographic Failures) and improper certificate validation (CWE-295, under A07:2025 Authentication Failures).

You will see what TLS guarantees and what it leaves open, choose settings that hold, close the first request with a redirect and HSTS, find mixed content in a page, and watch eight TLS clients connect or refuse in a recorded run.

TLS gives one connection three guarantees, and only that one

Open https://shop.example/account and TLS: Transport Layer Security, the protocol under HTTPS. It gives one connection confidentiality, integrity and an authenticated server; TLS 1.2 and 1.3 are the versions still allowed, since RFC 8996 retired 1.0 and 1.1., the protocol under HTTPS, protects that one connection. RFC 8446, which defines TLS 1.3, names what it provides: the server side is always authenticated, data sent after the handshake is visible only to the two endpoints, and it cannot be changed without detection. So a café network can carry the bytes but cannot read them, change them or pretend to be the shop.

Each guarantee rests on a check. The server proves that it holds the private key for a certificate naming shop.example, and the browser follows its certificate chain: A server's certificate plus the intermediate certificates that link it to a root certificate authority the client already trusts. A client checks every signature in the chain, the dates and the host name before it sends anything. to a certificate authority (CA) it already trusts; both sides agree on fresh keys for this connection; every record carries an integrity tag. Step through one visit and watch what the network sees at each step:

Interactive · message by message One HTTPS visit, and what the network sees Step through one visit to the shop's account page. The middle column is anyone on the path, such as the café Wi-Fi.
One HTTPS visit, and what the network sees, step 6 of 6. Messages so far: Your browser to shop.example: hello, shop.example; shop.example to Your browser: certificate chain; Your browser to shop.example: encrypted request; The network to shop.example: one byte changed.Your browserchecks passedThe networkanyone on the pathshop.examplerecord rejectedhello, shop.examplecertificate chainencrypted requestone byte changed

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Hello, with a name. The browser opens a TLS 1.3 connection and names the site it wants, shop.example, in its first message. That name and the server's address travel readable, so the network learns where you connect.
  2. A certificate and a key share. The server sends its certificate chain and its half of a fresh key exchange. Both sides now derive keys for this connection only, which are thrown away when it ends.
  3. Three checks. The browser checks that the chain ends at a CA it trusts, that the certificate is within its dates, and that it names shop.example. Only then does it send anything private.
  4. An encrypted request. The browser sends GET /account with its session cookie inside an encrypted record. The network sees how big the record is and when it was sent, but not a word of what it says.
  5. One byte changed. Someone on the path changes one byte of a record. Its integrity tag no longer matches, so the server rejects the record and ends the connection rather than act on altered data.
  6. What stays outside. TLS protected this connection only. The network still learned the name, the addresses and the sizes, the server still has to protect what it stores, and a client that skipped the three checks would have had none of this.

The last step is the one to keep: TLS protects data while it moves over this connection, and nothing else. It does not hide where you connect: the server’s address stays readable, and so does the host name in the first message, unless both ends support Encrypted Client Hello, which ASVS 5.0.0 asks for only at its highest level (12.1.5). RFC 8446 also says plainly that TLS does not hide the length of the data. It does nothing for the data once it has arrived: a database on the server holds it in the clear unless you protect it there, which the next lesson covers. And a client that skips the certificate checks gets none of the three guarantees, as the last section of this lesson shows.

Key idea

TLS gives one connection confidentiality, integrity and an authenticated server, and only while the client checks the certificate; the data before and after that connection needs protection of its own.

Checkpoint

Check: What TLS guarantees, and what it leaves open

  1. Question 1 of 2 Which of these does TLS give the connection between a browser and shop.example? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • The server is authenticated
    • Data that only the two endpoints can read
    • Changes on the way are detected

    TLS gives one connection confidentiality, integrity and an authenticated server. It hides neither where you connect nor how much you send, and it protects nothing once the data has arrived.

  2. Question 2 of 2 On café Wi-Fi, you open https://shop.example/account with a valid certificate. What can someone watching the Wi-Fi learn?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: That you connected to shop.example, and roughly how much data you sent and when

    TLS encrypts what is sent after the handshake, so the path, headers, cookies and body are hidden. What the connection is to, and how big and how frequent its records are, stays visible on the path.

Settings that hold: TLS 1.2 and 1.3, forward secrecy, automated certificates

Old versions of TLS are not a theoretical worry: RFC 8996 deprecated TLS 1.0 and 1.1 and says that neither may be used or negotiated. ASVS 5.0.0 requirement 12.1.1 asks that only current versions, such as 1.2 and 1.3, be enabled, with the newest preferred, and the A04:2025 prevention list asks for TLS 1.2 or newer with forward-secret cipher suites and no CBC mode. Walk through how the versions got here:

Timeline · 7 events in order From SSL 3.0 to TLS 1.3, and what comes next Walk through the versions in order. The tag says whether a version may still be enabled.
  1. SSL 3.0

    Retired by RFC 7568 never enable

    The last version of SSL. RFC 7568 deprecated it in 2015 and says it must not be used, and the OWASP TLS cheat sheet treats every SSL version as broken.

  2. TLS 1.0

    RFC 2246, from 1999 never enable

    The first version under the TLS name. RFC 8996 moved it to Historic status and says it must not be used or negotiated.

  3. TLS 1.1

    RFC 4346, from 2006 never enable

    A small revision of 1.0, retired by RFC 8996 together with it. Node.js clients refuse it with their default settings, as the run at the end of this lesson shows.

  4. TLS 1.2

    RFC 5246, from 2008 allowed

    Still allowed by ASVS 5.0.0 requirement 12.1.1. Keep it to forward-secret ECDHE key exchange with AEAD suites such as AES-GCM, and drop the CBC suites, as the A04:2025 text asks.

  5. TLS 1.3

    RFC 8446, from 2018 preferred

    Removed static RSA and Diffie-Hellman key exchange, so every public-key exchange is forward secret, and encrypts more of the handshake. Prefer it wherever both ends support it.

  6. RFC 8996

    TLS 1.0 and 1.1 deprecated, 2021 rule

    Best Current Practice 195 now forbids negotiating TLS 1.0 or 1.1 from any version, so a fallback to them is prohibited too. A server that still accepts them fails ASVS 12.1.1.

  7. Next

    Post-quantum key exchange prepare

    Hybrid groups such as X25519MLKEM768 combine a classical exchange with a post-quantum one. The A04:2025 text asks teams to prepare now so that high-risk systems are safe by the end of 2030.

The timeline has 7 events, from SSL 3.0 to Next.

Here is a configuration that meets those rules, one setting at a time:

Setting Use Avoid Source
Protocol versions TLS 1.3, and TLS 1.2 where clients need it SSL 3.0, TLS 1.0, TLS 1.1 RFC 8996; ASVS 12.1.1
Cipher suites AEAD suites: AES-GCM, ChaCha20-Poly1305 CBC, NULL, anonymous and export suites A04:2025; OWASP TLS cheat sheet
Key exchange ECDHE, such as X25519, and a hybrid post-quantum group where supported Static RSA or Diffie-Hellman RFC 8446; OWASP TLS cheat sheet
Certificates Publicly trusted, every host name listed, renewed by ACME Self-signed on public sites, renewal by hand ASVS 12.2.2; RFC 8555
Watching Expiry alerts and Certificate Transparency logs Hearing about it from users RFC 6962

The key exchange row is forward secrecy: A property of a key exchange in which every connection uses fresh keys that are discarded afterwards, so a server key stolen later cannot decrypt traffic recorded earlier. Every public-key exchange in TLS 1.3 has it.: each connection uses fresh keys that are thrown away afterwards, so a server key stolen next year cannot decrypt traffic recorded today. TLS 1.3 made every public-key exchange forward secret by removing the static RSA and Diffie-Hellman suites. The OWASP cheat sheet adds a hybrid post-quantum group, X25519MLKEM768, which mixes a classical exchange with a post-quantum one, and the A04:2025 text asks teams to start preparing now, so that high-risk systems are safe by the end of 2030 at the latest.

Certificates now expire quickly on purpose. A CA/Browser Forum ballot adopted in 2025 shortens the longest validity of a publicly trusted TLS certificate from 398 days to 47 days, in steps that end in 2029, so renewal by hand stops being an option. ACME, RFC 8555, is the protocol that lets a server obtain and renew its certificates without anyone touching them. Two kinds of watching complete the setup: an alert well before any certificate expires, and a look at Certificate Transparency: Public, append-only logs of the TLS certificates that certificate authorities issue (RFC 6962). Watching them shows a certificate for your domain that you never asked for. logs, public records of the certificates CAs issue, where a certificate for your domain that you never asked for shows up.

SSL Certificate Checker Check a domain you run: certificate trust, the redirect to HTTPS, HSTS and preload status, mixed content and its certificates in CT logs.

Checkpoint

Check: TLS versions, suites and certificates

  1. Question 1 of 2 A server accepts TLS 1.0, 1.1, 1.2 and 1.3, including CBC suites kept for old clients. What do RFC 8996, ASVS 12.1.1 and the A04:2025 text ask you to change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Turn off TLS 1.0, TLS 1.1 and the CBC suites, and keep TLS 1.3 preferred with TLS 1.2 on AEAD suites

    Enable only current versions, TLS 1.2 and 1.3 with 1.3 preferred, and on TLS 1.2 only AEAD suites with forward-secret ECDHE key exchange.

  2. Question 2 of 2 A server's private key is stolen. Someone recorded its HTTPS traffic a year ago, when it used ECDHE key exchange. Can they decrypt the recording now?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No, because each connection used fresh keys that were thrown away afterwards

    With ECDHE, each connection's keys come from a fresh exchange and are discarded. A stolen long-term key lets an impostor sign new handshakes, so rotate it at once, but recorded traffic stays sealed.

The first request is the weak point, and HSTS closes it

The weakest moment of an HTTPS site is the request before HTTPS. A customer types shop.example into the address bar, and a browser that has never visited the site sends http://shop.example/ in plain text. The server should answer with a permanent redirect to https://shop.example/ on the same host, as RFC 6797 section 7.2 advises, but that answer travels over plain HTTP as well, and hstspreload.org points out that someone on the path can rewrite the redirect to keep the browser on plain HTTP. HSTS: HTTP Strict Transport Security: a response header that tells a browser to reach a host only over HTTPS for max-age seconds and to refuse certificate errors there without a click-through. Browsers ignore it when it arrives over plain HTTP. More in “HTTP security headers: a baseline that holds” closes the gap for every visit after the first:

Interactive · message by message The first visit, the redirect and HSTS Step through a customer's first two visits to the shop. The middle column is anyone on the path.
The first visit, the redirect and HSTS, step 6 of 6. Messages so far: Your browser to shop.example: plain http request; shop.example to Your browser: 301 to https; shop.example to Your browser: HSTS header; Your browser to shop.example: https request.Your browserrefuses bad certificatesThe networkanyone on the pathshop.exampleredirects and sends HSTSplain http request301 to httpsHSTS headerhttps request

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Typed without https. The customer types shop.example. A browser that has never seen the site sends http://shop.example/ in plain text, which anyone on the path can read and change.
  2. The redirect. The server answers with a permanent redirect to https://shop.example/, on the same host. That answer is plain HTTP too, so someone on the path could rewrite it and keep the browser on HTTP.
  3. HTTPS and the header. The browser follows the redirect over HTTPS. The response carries Strict-Transport-Security with max-age=31536000 and includeSubDomains, and the browser stores the policy for a year.
  4. The next visit. A week later the customer clicks an http:// link to the shop. The browser rewrites it to https:// before sending anything, so no plain request leaves the device.
  5. Errors are final. On a host with a stored policy, a certificate error ends the connection, and the browser offers no way to click through. A warning that people learn to dismiss is gone.
  6. Only the first visit was exposed. HSTS could not protect step 1, because the browser had not seen the header yet. The preload list builds the policy into browsers so that even the first visit uses HTTPS, at the cost of a slow way back off the list.

RFC 6797 sets the rules a browser follows. It ignores Strict-Transport-Security that arrives over plain HTTP, reads only the first such header, requires max-age, and treats a directive that appears twice as an invalid header. On a host it knows, it ends the connection at any certificate error, with no way to click through. hstspreload.org adds a rule of its own for the list: every redirect served over HTTPS must carry the header itself. The program below reads eight headers the way a browser does, then checks each one against the preload list’s submission rules:

Eight HSTS headers, read the way a browser reads them JavaScript · hsts/hsts_check.mjs
// readHsts() reads a Strict-Transport-Security header the way RFC 6797 tells a browser to: only over HTTPS, max-age
// required, each directive at most once, values optionally quoted. preloadReady() then applies the submission rules
// of hstspreload.org to the policy and to two facts about the site: where its plain-HTTP redirect goes, and whether
// every subdomain answers over HTTPS.

function readHsts(value, overHttps) {
  if (!overHttps) return { ok: false, why: 'sent over plain HTTP, so the browser ignores it' };
  const seen = new Map();
  for (const part of value.split(';')) {
    const text = part.trim();
    if (!text) continue;
    const [rawName, ...rest] = text.split('=');
    const name = rawName.trim().toLowerCase();
    if (seen.has(name)) return { ok: false, why: `${name} appears twice, so the header is invalid` };
    seen.set(name, rest.length ? rest.join('=').trim().replace(/^"(.*)"$/, '$1') : true);
  }
  const maxAge = seen.get('max-age');
  if (maxAge === undefined) return { ok: false, why: 'no max-age, which the header requires' };
  if (!/^\d+$/.test(String(maxAge))) return { ok: false, why: `max-age "${maxAge}" is not a number of seconds` };
  return { ok: true, maxAge: Number(maxAge), subdomains: seen.has('includesubdomains'), preload: seen.has('preload') };
}

function preloadReady(policy, site) {
  const missing = [];
  if (!site.redirectsToHttpsSameHost) missing.push('redirect to HTTPS on the same host');
  if (!site.allSubdomainsHttps) missing.push('HTTPS on every subdomain');
  if (policy.maxAge < 31536000) missing.push('max-age of a year or more');
  if (!policy.subdomains) missing.push('includeSubDomains');
  if (!policy.preload) missing.push('preload');
  return missing;
}

const duration = (s) => (s < 3600 ? `${s / 60} minutes` : s < 86400 ? `${s / 3600} hours` : `${Math.round(s / 86400)} days`);
const ready = { redirectsToHttpsSameHost: true, allSubdomainsHttps: true };
const responses = [
  ['the baseline', 'max-age=31536000; includeSubDomains', true, ready],
  ['asking to be preloaded', 'max-age=63072000; includeSubDomains; preload', true, ready],
  ['the same, over plain HTTP', 'max-age=63072000; includeSubDomains; preload', false, ready],
  ['first stage, quoted value', 'max-age="300"; includeSubDomains', true, ready],
  ['no max-age', 'includeSubDomains; preload', true, ready],
  ['two max-age directives', 'max-age=31536000; max-age=0', true, ready],
  ['switching the policy off', 'max-age=0', true, ready],
  ['redirect goes to www first', 'max-age=63072000; includeSubDomains; preload', true, { ...ready, redirectsToHttpsSameHost: false }],
];

for (const [name, value, overHttps, site] of responses) {
  const policy = readHsts(value, overHttps);
  console.log(`${name}: ${value}`);
  if (!policy.ok) {
    console.log(`  ignored: ${policy.why}`);
    continue;
  }
  if (policy.maxAge === 0) {
    console.log('  removes the policy the browser stored for this host');
    continue;
  }
  const scope = policy.subdomains ? 'this host and every subdomain' : 'this host only';
  console.log(`  HTTPS only for ${duration(policy.maxAge)}, ${scope}`);
  const missing = preloadReady(policy, site);
  console.log(`  preload: ${missing.length ? `not ready, missing ${missing.join(', ')}` : 'meets every rule'}`);
}

Output

the baseline: max-age=31536000; includeSubDomains
  HTTPS only for 365 days, this host and every subdomain
  preload: not ready, missing preload
asking to be preloaded: max-age=63072000; includeSubDomains; preload
  HTTPS only for 730 days, this host and every subdomain
  preload: meets every rule
the same, over plain HTTP: max-age=63072000; includeSubDomains; preload
  ignored: sent over plain HTTP, so the browser ignores it
first stage, quoted value: max-age="300"; includeSubDomains
  HTTPS only for 5 minutes, this host and every subdomain
  preload: not ready, missing max-age of a year or more, preload
no max-age: includeSubDomains; preload
  ignored: no max-age, which the header requires
two max-age directives: max-age=31536000; max-age=0
  ignored: max-age appears twice, so the header is invalid
switching the policy off: max-age=0
  removes the policy the browser stored for this host
redirect goes to www first: max-age=63072000; includeSubDomains; preload
  HTTPS only for 730 days, this host and every subdomain
  preload: not ready, missing redirect to HTTPS on the same host

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node hsts_check.mjs

Three of the eight do nothing at all: the header sent over plain HTTP, the one without max-age and the one with max-age twice. max-age=0 is not a mistake but the off switch: it removes the policy the browser stored. Only one header meets every preload rule, and the last case shows a rule that catches teams out: a plain-HTTP redirect that goes straight to www.shop.example skips the HTTPS response of the base domain, which is where the header must be seen. The rollout itself, from five minutes up to a year, is in the security headers lesson, and the HSTS preload list: A list of domains built into browsers that are reached only over HTTPS, even on the first visit. Joining needs a year of max-age, includeSubDomains and preload, and leaving it reaches users only through browser updates. More in “HTTP security headers: a baseline that holds” stays a separate decision, because its maintainers recommend HSTS but no longer recommend preloading.

One directive

Press Edit, add ; preload to the baseline header in the first case and run it again. The preload line now says it meets every rule, because the baseline already has a year of max-age and includeSubDomains.

Checkpoint

Check: The first request and HSTS

  1. Question 1 of 2 A server sends Strict-Transport-Security only on its plain-HTTP responses, not on its HTTPS ones. What do browsers do with it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Ignore it, so the site gets no HSTS protection

    The header must be sent over HTTPS, on every HTTPS response, redirects included. Over plain HTTP, the server sends only the redirect.

  2. Question 2 of 2 http://shop.example/ redirects straight to https://www.shop.example/, and both HTTPS hosts send a valid preload header. Why does the preload check fail?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The plain-HTTP redirect must go to HTTPS on the same host first, where the base domain's header is seen

    The fix is http://shop.example/ → https://shop.example/ → https://www.shop.example/, with the header on every HTTPS response, the second redirect included.

One http: script can undo an HTTPS page

A padlock says the page itself arrived over TLS. It promises nothing about what the page loads next. MDN calls those requests mixed content: Parts of an HTTPS page, such as scripts, stylesheets, images or frames, that the page asks the browser to load over plain HTTP. Browsers upgrade images, audio and video to HTTPS and block the rest.: parts of an HTTPS page that are fetched over plain HTTP, where someone on the path can read or change them. A script is the worst case, because it can change anything on the page. Current browsers sort these requests into two groups: images, audio and video are upgraded to https by the browser, while scripts, stylesheets, frames, objects and images listed in srcset are blocked. The scanner below lists the http: addresses of one product page and what a browser does with each:

One product page, scanned for http: requests JavaScript · mixed/mixed_content.mjs
// findMixedContent() lists the http: addresses an HTTPS page asks the browser to load, and what a current browser does
// with each, following MDN's two groups: images, audio and video are upgraded to https (except from an IP address,
// which cannot be upgraded and is blocked); scripts, stylesheets, frames, objects and srcset images are blocked. A
// link is not mixed content, but it leaves HTTPS when clicked. The scan reads plain HTML with regular expressions,
// which is enough for a first inventory of your own templates, not a parser for HTML in general.

const UPGRADABLE = new Set(['img src', 'audio src', 'video src', 'source src']);
const BLOCKABLE = new Set(['script src', 'link href', 'iframe src', 'object data', 'img srcset']);

function findMixedContent(html) {
  const found = [];
  for (const [, tag, attrs] of html.matchAll(/<([a-z]+)\b([^>]*)>/gi)) {
    for (const [, name, value] of attrs.matchAll(/([a-z-]+)\s*=\s*"([^"]*)"/gi)) {
      const where = `${tag.toLowerCase()} ${name.toLowerCase()}`;
      const urls = name.toLowerCase() === 'srcset' ? value.split(',').map((c) => c.trim().split(/\s+/)[0]) : [value];
      for (const url of urls.filter((u) => /^http:\/\//i.test(u))) {
        const host = /^http:\/\/([^/:?#]+)/i.exec(url)[1];
        let action = 'not a request the page makes';
        if (where === 'a href') action = 'not mixed content: a link that leaves HTTPS';
        else if (UPGRADABLE.has(where)) action = /^[\d.]+$|^\[/.test(host) ? 'blocked: an IP address is not upgraded' : 'upgraded to https';
        else if (BLOCKABLE.has(where)) action = 'blocked';
        found.push({ where, url, action });
      }
    }
  }
  return found;
}

const page = `
<link rel="stylesheet" href="http://cdn.shop.example/site.css">
<script src="http://cdn.shop.example/cart.js"></script>
<script src="https://cdn.shop.example/search.js"></script>
<img src="http://cdn.shop.example/logo.png" alt="Shop logo">
<img src="http://192.0.2.10/banner.png" alt="Sale banner">
<img src="https://cdn.shop.example/hero.png" srcset="http://cdn.shop.example/hero-2x.png 2x" alt="Tea gardens">
<video src="http://media.shop.example/intro.mp4"></video>
<iframe src="http://reviews.example/widget"></iframe>
<a href="http://blog.shop.example/">Our blog</a>`;

const found = findMixedContent(page);
for (const f of found) console.log(`${f.where.padEnd(11)}${f.url.padEnd(38)}${f.action}`);
const count = (prefix) => found.filter((f) => f.action.startsWith(prefix)).length;
console.log(`\n${count('upgraded')} upgraded, ${count('blocked')} blocked, ${count('not')} link`);
console.log(`With upgrade-insecure-requests, all ${count('upgraded') + count('blocked')} requests go out over https; fix the templates anyway.`);

Output

link href  http://cdn.shop.example/site.css      blocked
script src http://cdn.shop.example/cart.js       blocked
img src    http://cdn.shop.example/logo.png      upgraded to https
img src    http://192.0.2.10/banner.png          blocked: an IP address is not upgraded
img srcset http://cdn.shop.example/hero-2x.png   blocked
video src  http://media.shop.example/intro.mp4   upgraded to https
iframe src http://reviews.example/widget         blocked
a href     http://blog.shop.example/             not mixed content: a link that leaves HTTPS

2 upgraded, 5 blocked, 1 link
With upgrade-insecure-requests, all 7 requests go out over https; fix the templates anyway.

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node mixed_content.mjs

Five of the seven requests are blocked, so the page loads without its stylesheet, its cart script, a banner, the large hero image and the reviews frame: in current browsers, mixed content mostly shows up as a broken page. The banner is blocked although it is an image, because its host is an IP address, which browsers do not upgrade. The link to the blog is not mixed content at all; it simply leaves HTTPS when clicked.

Fix When to use it
Change http:// to https://, or to a relative address, where the link is written Always: templates, stored HTML, configuration
Send Content-Security-Policy: upgrade-insecure-requests While the sources are being fixed: the browser requests every subresource over HTTPS
Serve the file from your own HTTPS host, or drop it When the other server has no HTTPS
Mark every cookie Secure Always: a cookie without it travels with any plain-HTTP request, the first one included

The last row is the cookie form of the same mistake. A session cookie without Secure goes out with any plain-HTTP request to the site, such as the very first request before the redirect, and the cookies lesson covers Secure and the __Host- prefix in full.

Checkpoint

Check: Mixed content and Secure cookies

  1. Question 1 of 2 An HTTPS page loads these from http: addresses. Which do current browsers block? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • A page in an iframe
    • A stylesheet in a link element
    • A script in a script element

    MDN's two groups: images, audio and video are upgraded to https; scripts, stylesheets, frames, objects and srcset images are blocked. In the lesson's page, 5 of 7 requests were blocked.

  2. Question 2 of 2 An HTTPS page shows a banner with <img src="http://192.0.2.10/sale.png">. What happens to it in a current browser?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It is blocked, because a host given as an IP address is not upgraded

    The fix is at the source: serve the banner from an HTTPS host name, as the scanner in the lesson suggests for every blocked request.

A client that skips certificate checks trusts anyone

Browsers check certificates without being asked. Code you write is a TLS client too: the job that calls a payment API, a service that sends webhooks, a script that runs curl. There the checks can be switched off with one option, and teams do it when a certificate from a private CA fails: rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0 in Node.js, verify=False in Python’s Requests, curl -k. Node’s documentation calls the variable insecure and strongly discourages it, and the Requests documentation says that with verify=False the client accepts any certificate. A client checks three things before it sends a byte:

Interactive · step by step The three checks a TLS client makes Step through the cases of the program below. Each check either passes the connection on or refuses it.
The three checks a TLS client makes, step 6 of 6. Showing Chain to a trusted CA?, Within its dates?, Names the right host?, Send the request (line 4: verified), Refuse to connect (lines 1, 5, 6 and 7). In focus: Within its dates?, Refuse to connect.yesyesyesnononoChain to a trusted CA?Within its dates?Names the right host?Send the requestline 4: verifiedRefuse to connectlines 1, 5, 6 and 7

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Default client, lab CA. The real server's certificate was signed by the lab CA, which is not on Node's bundled list of public CAs. The chain check fails, and the client refuses, line 1 of the run.
  2. Verification off. With rejectUnauthorized set to false the client skips every check and sends its request to whoever answered. Lines 2 and 3 connected to the real server and to the impostor alike.
  3. Trust the lab CA. Giving the client the lab CA with the ca option makes the chain check pass for certificates that CA signed. The dates are fine and the name is shop.example, so line 4 connects with every check on.
  4. The impostor. The impostor's certificate names shop.example too, but it is signed by itself, not by the lab CA. The chain check fails, and line 5 refuses.
  5. The wrong name. A client that asked for api.example gets a certificate that names only shop.example. The chain and the dates pass, the name check fails, and line 6 refuses.
  6. An expired certificate. A certificate signed by the lab CA whose last valid day has passed fails the dates check, line 7. Checking the chain alone would have let it through.

The program below makes a lab CA, a server certificate signed by it, an impostor’s self-signed certificate and an expired one, all in memory for each run, then connects eight clients through an in-memory pair of streams. The second tab is the helper that builds the certificates:

Eight TLS clients, one real server and one impostor

JavaScript (tls_checks.mjs) · handshake/tls_checks.mjs

// Eight TLS connections to a server for shop.example, each with client settings a team might write. Every server
// certificate is made in memory for this run (certs.mjs), and client and server talk through an in-memory pair of
// streams, so nothing touches the network. Each line says whether the client connected or refused, and why.
import { duplexPair } from 'node:stream';
import tls from 'node:tls';
import { makeCert } from './certs.mjs';

const labCa = makeCert({ subject: 'Lab Test CA', ca: true });
const real = makeCert({ subject: 'shop.example', issuer: labCa, hosts: ['shop.example'] });
const impostor = makeCert({ subject: 'shop.example', hosts: ['shop.example'] }); // signed by itself, not by the lab CA
const expired = makeCert({ subject: 'shop.example', issuer: labCa, hosts: ['shop.example'], validFromDays: -60, validToDays: -1 });

function connect(server, clientOptions) {
  return new Promise((done) => {
    const [serverSide, clientSide] = duplexPair();
    const context = tls.createSecureContext({ key: server.key, cert: server.cert });
    new tls.TLSSocket(serverSide, { isServer: true, secureContext: context }).on('error', () => {});
    const client = tls.connect({ socket: clientSide, servername: 'shop.example', ...clientOptions });
    client.on('secureConnect', () => {
      done(`connected over ${client.getProtocol()}, verified: ${client.authorized ? 'yes' : 'no'}`);
      client.destroy();
    });
    client.on('error', (e) => done(`refused: ${e.code}`));
  });
}

const cases = [
  ['default client, real server', real, {}],
  ['verification off, real server', real, { rejectUnauthorized: false }],
  ['verification off, impostor', impostor, { rejectUnauthorized: false }],
  ['lab CA trusted, real server', real, { ca: [labCa.cert] }],
  ['lab CA trusted, impostor', impostor, { ca: [labCa.cert] }],
  ['lab CA trusted, asked for api.example', real, { ca: [labCa.cert], servername: 'api.example' }],
  ['lab CA trusted, expired certificate', expired, { ca: [labCa.cert] }],
  ['lab CA trusted, client stuck on TLS 1.1', real, { ca: [labCa.cert], minVersion: 'TLSv1', maxVersion: 'TLSv1.1' }],
];
for (const [i, [name, server, options]] of cases.entries()) {
  console.log(`${i + 1}. ${name.padEnd(42)}${await connect(server, options)}`);
}

Output

1. default client, real server               refused: UNABLE_TO_VERIFY_LEAF_SIGNATURE
2. verification off, real server             connected over TLSv1.3, verified: no
3. verification off, impostor                connected over TLSv1.3, verified: no
4. lab CA trusted, real server               connected over TLSv1.3, verified: yes
5. lab CA trusted, impostor                  refused: DEPTH_ZERO_SELF_SIGNED_CERT
6. lab CA trusted, asked for api.example     refused: ERR_TLS_CERT_ALTNAME_INVALID
7. lab CA trusted, expired certificate       refused: CERT_HAS_EXPIRED
8. lab CA trusted, client stuck on TLS 1.1   refused: ERR_SSL_NO_PROTOCOLS_AVAILABLE

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node tls_checks.mjs

JavaScript (certs.mjs) · handshake/certs.mjs

// makeCert(): a throwaway test certificate, built in memory for one run of tls_checks.mjs and never stored. It writes
// the few X.509 fields a TLS client checks (issuer, validity dates, subject, the names it covers, whether it is a CA)
// in ASN.1 DER and signs them with an ECDSA P-256 key. Real certificates come from a CA through ACME; this exists only
// so the lab needs no key file on disk.
import { generateKeyPairSync, randomBytes, sign } from 'node:crypto';

const len = (n) => (n < 128 ? [n] : n < 256 ? [0x81, n] : [0x82, n >> 8, n & 255]);
const tlv = (tag, ...parts) => {
  const body = Buffer.concat(parts);
  return Buffer.concat([Buffer.from([tag, ...len(body.length)]), body]);
};
const seq = (...parts) => tlv(0x30, ...parts);
const oid = (dotted) => {
  const [a, b, ...rest] = dotted.split('.').map(Number);
  const bytes = [40 * a + b];
  for (let v of rest) {
    const chunk = [v & 127];
    while ((v = Math.floor(v / 128))) chunk.unshift((v & 127) | 128);
    bytes.push(...chunk);
  }
  return tlv(0x06, Buffer.from(bytes));
};
const integer = (buf) => tlv(0x02, buf[0] & 0x80 ? Buffer.concat([Buffer.from([0]), buf]) : buf);
const utcTime = (date) => tlv(0x17, Buffer.from(`${date.toISOString().replace(/[-:T]/g, '').slice(2, 14)}Z`));
const commonName = (cn) => seq(tlv(0x31, seq(oid('2.5.4.3'), tlv(0x0c, Buffer.from(cn)))));
const ECDSA_WITH_SHA256 = seq(oid('1.2.840.10045.4.3.2'));
const DAY = 86_400_000;

export function makeCert({ subject, issuer = null, hosts = [], ca = false, validFromDays = -1, validToDays = 30 }) {
  const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
  const extensions = [];
  if (ca) extensions.push(seq(oid('2.5.29.19'), tlv(0x01, Buffer.from([0xff])), tlv(0x04, seq(tlv(0x01, Buffer.from([0xff]))))));
  if (hosts.length) extensions.push(seq(oid('2.5.29.17'), tlv(0x04, seq(...hosts.map((h) => tlv(0x82, Buffer.from(h)))))));
  const serial = randomBytes(8);
  serial[0] &= 0x7f;
  const now = Date.now();
  const tbs = seq(
    tlv(0xa0, integer(Buffer.from([2]))),
    integer(serial),
    ECDSA_WITH_SHA256,
    commonName(issuer ? issuer.subject : subject),
    seq(utcTime(new Date(now + validFromDays * DAY)), utcTime(new Date(now + validToDays * DAY))),
    commonName(subject),
    publicKey.export({ type: 'spki', format: 'der' }),
    ...(extensions.length ? [tlv(0xa3, seq(...extensions))] : []),
  );
  const signature = sign('sha256', tbs, issuer ? issuer.privateKey : privateKey);
  const der = seq(tbs, ECDSA_WITH_SHA256, tlv(0x03, Buffer.from([0]), signature));
  const cert = `-----BEGIN CERTIFICATE-----\n${der.toString('base64').match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----\n`;
  return { subject, cert, privateKey, key: privateKey.export({ type: 'pkcs8', format: 'pem' }) };
}

The default client refused the real server, because Node trusts its bundled list of public CAs and the lab CA is not on it. Switching verification off made the connection work, and line 3 shows the price: the same client connected to the impostor just as readily, and an impostor is exactly what someone on the path would present. The fix is line 4: give the client the CA it should trust through the ca option, which replaces the default list for that connection, and every check stays on. Lines 5 to 7 are those checks doing their work, and line 8 is a client stuck on TLS 1.1, which Node’s defaults refuse.

Switching verification off to fix a private CA

Wrong: rejectUnauthorized: false, verify=False, curl -k, or NODE_TLS_REJECT_UNAUTHORIZED=0 in a deployment file. Right: hand that one client the CA it should trust, with ca in Node.js, verify="/path/to/ca.pem" in Requests or --cacert in curl. The wrong version connects to any server that answers; the right one connects only to servers the CA vouched for.

The same holds inside your own network. ASVS 5.0.0 asks for TLS on internal connections as well (12.3.1 and 12.3.3), for clients that validate certificates (12.3.2), and, for internally issued certificates, for callers that trust only those internal CAs (12.3.4). A network you own is still a network that others can reach.

Key idea

A TLS client is only as safe as its checks: trust the right CA for each connection instead of switching verification off, and an impostor is refused like any other wrong certificate.

Checkpoint

Check: Certificate checks in your own clients

  1. Question 1 of 2 A partner's API uses a certificate from the partner's own private CA, and your Node.js job fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE. What is the right fix?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Give that client the partner's CA certificate with the ca option, and keep verification on

    Trust the CA that should vouch for this server, for this client only. In Requests that is verify with the CA file; in curl, --cacert.

  2. Question 2 of 2 In the lesson's run, line 3 connected to the impostor. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: With verification off, the client accepts any certificate, the impostor's too

    Turning verification off does not fix a certificate problem; it removes the checks that tell the real server from anyone else.

Practise: an HSTS policy and a TLS client that checks

Write three functions: the Strict-Transport-Security value for a policy, the response a server sends over plain HTTP and over HTTPS, and the options a team’s settings turn into for a TLS client. The sample tests run in your browser:

Exercise · Easy · JavaScript

An HSTS policy, a redirect and a TLS client that checks

Three functions in transport.mjs decide how a small shop uses TLS. The starter gets each one wrong. Fix all three:

  1. hstsValue(policy) returns the value of the Strict-Transport-Security header for { maxAge, includeSubDomains, preload }: max-age=<seconds>, then ; includeSubDomains when it is set, then ; preload when it is set. It throws an Error when maxAge is not a whole number of seconds from 0 up, and when preload is asked for without includeSubDomains or with a maxAge under 31536000 (one year).
  2. respond(request, policy) answers a request { scheme, host, path }, where path includes any query string. Over plain HTTP it returns { status: 301, headers: { Location: 'https://' + host + path } }, a redirect on the same host and no Strict-Transport-Security header. Over HTTPS it returns { status: 200, headers: { 'Strict-Transport-Security': hstsValue(policy) } }.
  3. clientOptions(settings) turns a team's settings { host, ca, minVersion, allowInvalid } into options for Node's tls.connect: { host, servername: host, minVersion, rejectUnauthorized: true }, with minVersion 'TLSv1.2' when the settings give none, and ca: [settings.ca] only when the settings name a CA certificate. It throws an Error when allowInvalid is true and when minVersion is 'TLSv1' or 'TLSv1.1': the fix for a private CA is to trust that CA, never to switch the checks off.

Starter code · transport.mjs

// Starter: each function has the mistake a team might make. Fix all three.

export function hstsValue(policy) {
  return `max-age=${policy.maxAge}`;
}

export function respond(request, policy) {
  return { status: 200, headers: { 'Strict-Transport-Security': hstsValue(policy) } };
}

export function clientOptions(settings) {
  return { host: settings.host, rejectUnauthorized: !settings.allowInvalid };
}
The sample tests · transport.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { clientOptions, hstsValue, respond } from './transport.mjs';

const year = { maxAge: 31536000, includeSubDomains: true };

test('the header names max-age and the directives that are set', () => {
  assert.equal(hstsValue({ maxAge: 300 }), 'max-age=300');
  assert.equal(hstsValue(year), 'max-age=31536000; includeSubDomains');
  assert.equal(hstsValue({ maxAge: 0 }), 'max-age=0', 'zero is the off switch, and valid');
});

test('max-age must be a whole number of seconds', () => {
  assert.throws(() => hstsValue({ maxAge: -1 }), undefined, 'a negative number');
  assert.throws(() => hstsValue({ maxAge: 1.5 }), undefined, 'a fraction');
  assert.throws(() => hstsValue({ maxAge: '300' }), undefined, 'a string');
});

test('preload needs a year of max-age and includeSubDomains', () => {
  assert.equal(hstsValue({ ...year, preload: true }), 'max-age=31536000; includeSubDomains; preload', 'exactly one year is enough');
  assert.equal(hstsValue({ maxAge: 63072000, includeSubDomains: true, preload: true }), 'max-age=63072000; includeSubDomains; preload');
  assert.throws(() => hstsValue({ maxAge: 31535999, includeSubDomains: true, preload: true }), undefined, 'a second short of a year');
  assert.throws(() => hstsValue({ maxAge: 31536000, preload: true }), undefined, 'without includeSubDomains');
});

test('plain HTTP gets a redirect on the same host, and no HSTS header', () => {
  const r = respond({ scheme: 'http', host: 'shop.example', path: '/cart?id=7' }, year);
  assert.equal(r.status, 301);
  assert.equal(r.headers.Location, 'https://shop.example/cart?id=7');
  assert.equal(r.headers['Strict-Transport-Security'], undefined, 'browsers ignore it over plain HTTP');
});

test('HTTPS responses carry the header', () => {
  const r = respond({ scheme: 'https', host: 'shop.example', path: '/' }, year);
  assert.equal(r.status, 200);
  assert.equal(r.headers['Strict-Transport-Security'], 'max-age=31536000; includeSubDomains');
});

test('client options keep every check on and trust the named CA', () => {
  assert.deepEqual(clientOptions({ host: 'api.partner.example' }), { host: 'api.partner.example', servername: 'api.partner.example', minVersion: 'TLSv1.2', rejectUnauthorized: true });
  const withCa = clientOptions({ host: 'api.partner.example', ca: 'PARTNER-CA-PEM', minVersion: 'TLSv1.3' });
  assert.deepEqual(withCa, { host: 'api.partner.example', servername: 'api.partner.example', minVersion: 'TLSv1.3', rejectUnauthorized: true, ca: ['PARTNER-CA-PEM'] });
  assert.throws(() => clientOptions({ host: 'api.partner.example', allowInvalid: true }), undefined, 'never switch the checks off');
  assert.throws(() => clientOptions({ host: 'api.partner.example', minVersion: 'TLSv1.1' }), undefined, 'TLS 1.1 is deprecated');
  assert.throws(() => clientOptions({ host: 'api.partner.example', minVersion: 'TLSv1' }), undefined, 'TLS 1.0 is deprecated');
});
A hint

Check the policy before you build the string: Number.isInteger(maxAge) && maxAge >= 0 first, then the two preload rules. In respond, decide on the scheme first, so the plain-HTTP answer never reaches the line that adds the header.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

SSL Certificate Decoder Paste a certificate or a whole chain to see its names, dates and issuer, and whether the chain is in order.

Finish with the self-check below. It starts with the transport idea where your answers are weakest and moves on as they come out right:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which of these does TLS leave visible to someone on the path?

    Choose one answer.

    A hint

    Think about what a network can measure without reading anything.

    Show the answer: Question 1 of 10

    Answer: The size and timing of the encrypted records

    Sizes and timing, the addresses and, without Encrypted Client Hello, the host name stay visible. Everything sent after the handshake is encrypted and protected against change.

  2. Question 2 of 10 A teammate argues that the database backups need no encryption because every request to the site already uses HTTPS. What is wrong with the argument?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: HTTPS protects data only while it moves over a connection, and a backup is data at rest

    Protection in transit and protection at rest are separate jobs. The next lesson covers what to store, mask and encrypt.

  3. Question 3 of 10 Which of these are AEAD encryption choices that the A04:2025 text and the OWASP TLS cheat sheet expect? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 3 of 10

    Answer:

    • ChaCha20-Poly1305
    • AES-GCM

    AEAD suites encrypt and authenticate in one step. TLS 1.3 offers only AEAD suites; on TLS 1.2, enable only those, with ECDHE key exchange.

  4. Question 4 of 10 Public TLS certificate lifetimes are falling in steps to 47 days. What should a team that renews certificates by hand once a year do?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Automate issuance and renewal with ACME, and alert well before any certificate expires

    ACME (RFC 8555) obtains and renews certificates automatically. Pair it with expiry alerts and a watch on Certificate Transparency logs.

  5. Question 5 of 10 A browser receives Strict-Transport-Security max-age=0 over HTTPS. What does it do?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Removes the HSTS policy it stored for that host

    max-age=0 sent over HTTPS is how a site tells each browser that remembers it to forget the policy.

  6. Question 6 of 10 The main domain sends HSTS with includeSubDomains and a year of max-age. An internal subdomain still works only over plain HTTP. What happens to people who visited the main domain?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Their browsers refuse plain HTTP to that subdomain for up to a year, so it stops working for them

    Check every subdomain before sending includeSubDomains, and roll the header out in stages, as the security headers lesson describes.

  7. Question 7 of 10 A site adds Content-Security-Policy upgrade-insecure-requests while its templates still hold http: links. What changes for its pages?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: The browser requests every subresource over https, blockable ones included

    Use it while the sources are fixed, and fix them anyway: a resource whose host has no HTTPS fails even after the upgrade.

  8. Question 8 of 10 A site serves every page over HTTPS and redirects port 80 to HTTPS, but its session cookie has no Secure attribute. When can the cookie still travel in plain text?
    Show the answer: Question 8 of 10

    Answer: On any plain-HTTP request to the site, such as a typed first visit before the redirect

    Mark every cookie Secure, and prefer the __Host- prefix for session cookies, as the cookies lesson shows.

  9. Question 9 of 10 Which of these switch certificate verification off? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 9 of 10

    Answer:

    • curl -k
    • verify=False in Requests
    • rejectUnauthorized: false in Node.js
    • NODE_TLS_REJECT_UNAUTHORIZED=0

    The first four remove the checks; the last two change which CA the checks trust, which is the fix.

  10. Question 10 of 10 Two of your services talk over plain HTTP inside a private network, and a design note says TLS is not needed there. What does ASVS 5.0.0 ask for?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: TLS on internal connections too, with clients that validate certificates and trust only the internal CAs they need

    A private network is still reachable by anything that gets inside it. Treat internal calls like external ones: TLS, verified, with a narrow set of trusted CAs.

Interview questions

Warm-up: what does HTTPS protect, and what does it leave open?

HTTPS is HTTP over TLS, and TLS gives one connection three guarantees: the server is authenticated by a certificate that chains to a trusted CA and names the host, the data is readable only by the two endpoints, and any change on the way is detected. It leaves several things open. The server’s address and the host name in the first message are visible, and so are the size and timing of the traffic. It protects nothing before the connection, such as a first visit over plain HTTP, which is why sites redirect and send HSTS, and nothing after it: data stored on the server needs its own protection. And a client that switches certificate checks off loses all three guarantees, because it will talk to an impostor as readily as to the real server.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to senior

    A partner's API presents a certificate from the partner's own private CA. A teammate made the integration work by setting rejectUnauthorized to false. What do you change, and how do you stop it from coming back?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A content site with 4,000 articles moved to HTTPS, and the browser console shows mixed content on many pages, mostly images and embeds written into the stored article HTML. How do you fix it, and what do you do while the fix runs?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What does TLS guarantee for one connection, and what does it not cover?

    The server is authenticated, data sent after the handshake is visible only to the two endpoints, and it cannot be changed without detection (RFC 8446). It does not hide the server's address or the host name in the first message unless both ends use Encrypted Client Hello, it does not hide the length of the data, and it does nothing for data before or after the connection.

  2. Which TLS settings hold today?

    Versions: TLS 1.3, and 1.2 where clients need it (RFC 8996 deprecated 1.0 and 1.1; ASVS 5.0.0 12.1.1). Ciphers: AEAD suites such as AES-GCM and ChaCha20-Poly1305, never CBC, NULL, anonymous or export suites. Key exchange: ECDHE such as X25519, with a hybrid post-quantum group where supported, never static RSA. Certificates: publicly trusted, every host name listed, renewed by ACME, with expiry alerts and Certificate Transparency monitoring.

  3. What is forward secrecy?

    Each connection uses fresh keys that are thrown away afterwards, so a server key stolen next year cannot decrypt traffic recorded today. TLS 1.3 made every public-key exchange forward secret by removing the static RSA and Diffie-Hellman suites.

  4. Why is renewing TLS certificates by hand ending?

    A CA/Browser Forum ballot adopted in 2025 shortens the longest validity of a publicly trusted TLS certificate from 398 days to 47 days, in steps that end in 2029. ACME (RFC 8555) lets a server obtain and renew certificates without anyone touching them. Alert well before expiry, and watch Certificate Transparency logs for certificates you never asked for.

  5. Why is the first plain-HTTP request the weak point, and what does HSTS do about it?

    A browser that has never visited sends http://shop.example/ in plain text, and someone on the path can rewrite the redirect to keep it on HTTP. After one HTTPS response with Strict-Transport-Security, the browser goes straight to HTTPS and ends the connection at any certificate error, with no click-through. Redirect on the same host, and send the header on every HTTPS response.

  6. Which HSTS headers does a browser ignore, and what is max-age=0?

    It ignores the header over plain HTTP, reads only the first such header, requires max-age, and treats a directive that appears twice as an invalid header. max-age=0 is the off switch that removes the stored policy. For the preload list every redirect served over HTTPS must carry the header too, so a redirect from the base domain straight to www skips the response where it must be seen.

  7. What is mixed content, and how do you fix it?

    Parts of an HTTPS page fetched over plain HTTP. Current browsers upgrade images, audio and video (except on an IP address) and block scripts, stylesheets, frames, objects and srcset images, so it mostly shows up as a broken page. Fix the link where it is written (https, or a relative address); upgrade-insecure-requests helps meanwhile; serve the file yourself or drop it; and mark every cookie Secure.

  8. What do you do when a client must trust a private CA, and why not switch verification off?

    Give that one client the CA it should trust (ca in Node.js, verify="/path/to/ca.pem" in Requests, --cacert in curl) and leave every check on. rejectUnauthorized: false, verify=False, curl -k and NODE_TLS_REJECT_UNAUTHORIZED=0 connect to any server that answers, an impostor included. ASVS 5.0.0 asks for TLS and validating clients on internal connections too (12.3.1 to 12.3.4).

Key takeaways

  • TLS gives one connection confidentiality, integrity and an authenticated server; host names, sizes and the data at either end stay outside it.
  • Enable only TLS 1.2 and 1.3 with AEAD suites and forward-secret key exchange, and let ACME renew certificates whose lifetimes are falling to 47 days.
  • The first plain-HTTP request is the weak point: redirect it on the same host, send HSTS on every HTTPS response, and treat preloading as a separate decision.
  • Mixed content is fixed where links are written; upgrade-insecure-requests helps while that happens, and every cookie needs Secure.
  • A client that switches certificate checks off connects to an impostor too; trust the right CA for that connection and keep every check on.

Terms in this lesson

certificate chain
A server's certificate plus the intermediate certificates that link it to a root certificate authority the client already trusts. A client checks every signature in the chain, the dates and the host name before it sends anything.
Certificate Transparency
Public, append-only logs of the TLS certificates that certificate authorities issue (RFC 6962). Watching them shows a certificate for your domain that you never asked for.
forward secrecy
A property of a key exchange in which every connection uses fresh keys that are discarded afterwards, so a server key stolen later cannot decrypt traffic recorded earlier. Every public-key exchange in TLS 1.3 has it.
mixed content
Parts of an HTTPS page, such as scripts, stylesheets, images or frames, that the page asks the browser to load over plain HTTP. Browsers upgrade images, audio and video to HTTPS and block the rest.
TLS
Transport Layer Security, the protocol under HTTPS. It gives one connection confidentiality, integrity and an authenticated server; TLS 1.2 and 1.3 are the versions still allowed, since RFC 8996 retired 1.0 and 1.1.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.