denyfirst.

What the web check
sends, and what it cannot see.

This page has two readers. One is looking at a report and wants to know what it means. The other found this address in their access log, because this is what the request said it was, and wants to know exactly what reached their server. The second question is answered first.

What was sent to your server

If you are here from a log line, this is the whole of it. There is nothing else to find, and none of it is a probe for a weakness.

Every request names itself. The User-Agent header reads porch/1 (+https://denyfirst.dev/web/method), from every installation of the tool, so a line in your log leads here.

One GET of /, over HTTPS and over plaintext. Nothing else was requested. No path was constructed by this program at any point: there is no attempt at /admin, no guessing under /.well-known, and no second guess of any kind. This reads what a server volunteers to every visitor.

Redirects were followed, up to five, and only where a Location header named. Each address after the first was chosen by your server rather than by this scanner. A Location in another scheme is not followed, an overlong one is not followed, and credentials in one are stripped before the request is made. Where the chain stopped, the report says whether it ended or was declined, because a reader who cannot tell those apart cannot read the chain at all.

A redirect off what this deployment may reach is not followed. Whatever decides which hosts an installation of this tool connects to is asked again about the address a Location header names, before anything is dialled — because that address was chosen by a server rather than by the person running the scan. On this deployment that means the hosts this project owns, so a redirect away from them ends the chain and the report says so.

Whether the body was read depends on which installation reached you, so this says both. The user agent names this address from every installation of the tool, so one flat sentence here would be true of one of them and false of the one in your log.

This deployment never reads a body. Headers were taken and the body was closed unread, so the traffic cost you a header's worth. The restriction is compiled in rather than configured: this build refuses to read a body at all, whatever it is asked, and there is no setting that changes it.

An installation somebody runs themselves may read the page — once, to a bound of one megabyte, streamed, and only the page at the address whose headers it already fetched.

Where a page is read, nothing of it is kept. No link is followed, no script retrieved, and nothing else on your server is asked for. What survives the read is a short list of facts — whether a Content-Security-Policy was declared in the markup, and the host names of what the page pulls in from somewhere other than itself: anything loaded over plaintext, and anything loaded from another origin. A page that loads only its own things from its own host produces an empty list, because nothing asks about that.

A host, and never an address. The path is dropped, the query is dropped, and any credentials in it are dropped before anything else — http://user:token@host/ in somebody's markup is a credential, and a report carrying it would publish it to everyone the report is shown to. There is no field anywhere in a report that can hold markup, in the same way there is no field that can hold a cookie's value. The reason for reading the page at all is that mixed content, a script from elsewhere with nothing pinning it, and a policy declared with <meta http-equiv> are ordinary things a site has and nobody can see from headers; the reason nothing else is kept is that a page holds keys, tokens and names, and a report is a thing people paste into issue trackers.

Only the headers this check grades were kept. An allow list, not a deny list. Whatever else your server sent — internal host names, software versions, request identifiers — was discarded where the response was parsed rather than filtered out of the report later.

No cookie value was recorded, and there is nowhere to put one. The report holds a cookie's name and the attributes that decide whether it is safe. There is no field for a value at all: a value is a session identifier as often as not, and a report is a thing people paste into issue trackers.

Ports 80 and 443 only, and nothing was sent to any address that is private, loopback, link-local or reserved — including an address reached by following one of your own redirects.

If you would rather this domain were not scanned at all, that is arranged by asking and needs no explanation. The address, and what else a person here will answer, are under stopping a scan.

How to read a report

A report says four kinds of thing, and telling them apart is most of knowing what it means.

Findings are graded. Each one names a rule, states what was measured, and cites the document it rests on. Grading is worst-case: a site reached in the clear is reached in the clear however sound its policy declaration is.

Observed is what the check established and deliberately does not grade. The length of an HSTS max-age is the clearest case: no standards body publishes a minimum, and OWASP explicitly recommends a short one during a rollout, so the value is described in years or days along with what a browser does when it lapses — and never failed against a threshold this project invented. The same holds for a missing includeSubDomains on a host with nothing beneath it, and for a temporary redirect from the plaintext address, which works.

Not established for this host is what this particular scan could not settle, for a reason that lies with the server in front of it. These qualify that report's verdict and travel with it.

Limits of this method are below. They are true of every scan this check runs, so they are stated once, here, rather than repeated on every report where they read as faults of the server being looked at.

Two distinctions decide more than they look. A host that answered nothing is ungraded, not weak: declaring no policy is a claim about a server, and nothing was ever spoken to. And a host with nothing listening on port 80 is the safest arrangement there is, which is not the same fact as a host answering 200 in the clear, though both produce an empty list of headers.

The policy graded is the last one carried by a hop made over TLS, because that is the one a browser would end up holding. Reading the last hop describes a policy no browser holds the moment a site downgrades at the end of its chain; reading the first describes one a later hop replaced.

Only the root was asked

One request was made, to the root of the site, and no other address on it was asked for. Another page may answer with different headers and load different things, and nothing here describes any page but this one.

No browser ran here

Nothing was executed. Where the page was read, what it says it loads was read out of the markup — so anything a script fetches once it runs, and anything assembled after the page arrives, was not seen. Whether a declared policy is enforced in practice is visible only to a browser, and no browser ran here.

One answer, from one machine, at one moment

A name served by several machines, or one running an experiment, can answer the next visitor differently. This is what one address said once.

What else is written down

This check has its own rule set, separate from the one that grades a TLS handshake, because they answer different questions over different evidence and a single name for both would make two incomparable things look comparable. What the other one can and cannot see is on its own page.

Every property this project holds itself to is in docs/invariants.md, each one naming the test that guards it and, where there was one, the mistake that produced it. What changed between rule sets is in docs/policy-changes.md, because a report graded under one version is not comparable with a report graded under another.

What is recorded about a scan here, and how to have a domain excluded, are on the privacy page.