Skip to content

Security

What is actually implemented — and what is not.

This page describes controls that exist in the code today. It deliberately avoids the usual vendor vocabulary, because Corriden holds no audit report and a security claim that collapses under a diligence question is worth less than no page at all. Where something is absent, it is named as absent.

The useful starting point is that Corriden’s analysis runs on records the appraisal districts already publish to the public. The most effective control over a dataset is not holding sensitive data in it, and that is the shape of this one.

The parcel database holds no owner names or addresses

The table behind the parcel lookup and every county page stores what identifies a property on the public roll — county, account number, situs address, city, asset and property class, neighbourhood code, year built, building and land area, land, improvement and total value, and the roll year. There is no owner-name column and no owner-mailing-address column for anything to leak from. It is a constraint on the schema, not a filter applied on the way out.

Corriden does hold owner names and mailing addresses from the same public record for its direct-mail programme, as the about page states plainly. Those live in separate tables that only operator tooling can reach, are used to send that letter, and are never exposed to the website or sold.

A public key reads nothing

Row-level security is enabled on every table, and the parcel tables carry no read policies at all — so the public browser key returns nothing from them by default, rather than returning everything unless a rule intervenes. Every read the site performs happens in server code holding a privileged key.

That privileged key is confined to a module marked server-only, which turns any accidental import from browser code into a build failure rather than a silent leak. It cannot end up in a page bundle without the deployment breaking first.

Where a signed-in customer does need to read their own data — orders, watched parcels, reminder settings, subscriptions — the policy is scoped to that person: rows match on their user id or the email their sign-in carries. No table grants insert, update or delete to a signed-in user anywhere. Every write goes through server code.

Orders, packets and downloads

An order record holds what a purchase requires and no more: the email used at checkout, the Stripe session and payment references, the county and account of the parcel, the amount and currency Stripe reported, the status, and the storage path of the delivered packet.

A packet download is checked twice. The order is first looked up using your own signed-in session, so the row-level policy has to agree the order is yours — the privileged key is not used to answer the question “whose order is this?”. Only then is a short-lived signed link minted against a private storage bucket, valid for ten minutes. Packet files are not publicly addressable, and a link that leaks stops working quickly. An order that is not yours is indistinguishable from one that does not exist.

Payments never touch Corriden's servers

Checkout hands off to Stripe’s own hosted payment page. Card number, expiry and security code are entered on Stripe’s domain and are never transmitted to, processed by, or stored on Corriden’s servers — Corriden receives back only Stripe’s references and the amount. Prices are Stripe Price objects rather than amounts computed at request time, so a charge cannot be manipulated by tampering with a request.

The address a customer is returned to after paying is drawn from an allowlist of Corriden’s own hosts, never from the browser’s request headers. Without that, someone could obtain a genuine Corriden-branded Stripe page whose post-payment redirect landed the payer on a copy of the site. Stripe error details are never relayed to the browser.

Email you can stop, and links that survive scanners

Every reminder email carries an unsubscribe link and a manage link. Both are authenticated by a keyed signature over the recipient’s address, compared in constant time, so neither can be forged or edited to act on somebody else’s address — and neither requires an account to use.

Following an unsubscribe link changes nothing on its own; only the confirmation, or a mail client’s own one-click unsubscribe, actually acts. That split exists because corporate mail gateways pre-fetch every link in an inbound message, and a link that acts on being fetched will unsubscribe people who never clicked anything. Unsubscribing sets an account-level flag that every sending path checks, so watching another parcel later cannot quietly restart email; resuming is always a deliberate action.

How the roll data is obtained

Every fetch is an anonymous, unauthenticated request for a file or page the appraisal district publishes for public download. There are no district credentials anywhere in the codebase, nothing logs in, no access control or challenge is negotiated or circumvented, and there is no headless browser driving a site. If a record is not published to the public, Corriden does not have it.

The ingest is also built to fail loudly. Each roll’s expected columns are pinned in code: if a district changes its layout and an expected column is missing, the load raises an error and stops. It never quietly imports a partial or misaligned file — which matters, because a silent column shift would corrupt the comparison a customer is paying to rely on. Row counts are re-checked after loading, and no roll data is ever committed to the source repository.

Keys, secrets and the site itself

Credentials are supplied to the application as environment variables and are not stored in the repository; environment files are excluded from source control. Shared secrets and signature checks use constant-time comparison rather than ordinary string equality, so a comparison cannot be probed a character at a time. Structured data embedded in pages is escaped on output, closing off script injection through it. The endpoints behind the watchlist and the portfolio analyser apply a per-client rate limit.

Analytics are Plausible: no cookies, no personal data, aggregate page counts only. What Corriden stores about you, and how to have it deleted, is set out in the privacy policy.

What Corriden does not claim

Everything above is a control that exists. These are the ones that do not, stated so that nothing on this page is read as implying them.

No SOC 2, ISO 27001, or comparable audit

Corriden has not been audited against any security framework and holds no attestation report. If your procurement process requires one, Corriden will not satisfy it today, and it is better to learn that here than three weeks in.

No third-party penetration test

No external firm has been engaged to test this application.

No bug-bounty programme

Reports are welcome and read, but no payment is offered for them.

No PCI scope to speak of

That is a consequence of design rather than an achievement: card details are entered on Stripe’s own page, so they never reach Corriden’s servers to be protected in the first place.

No uptime or response-time commitment

There is no service-level agreement, and none is implied anywhere on this site.

Reporting a vulnerability

If you find a weakness, please report it to support@corriden.com with enough detail to reproduce it. Reports go to the same person who wrote the code, and are welcome — including ones that turn out to be mistaken.

Two requests while you look: please do not access, alter or retain data belonging to anyone else, and please do not degrade the service for other users — a proof of concept against your own account or a parcel record is enough to demonstrate almost anything. Please also give a reasonable interval before publishing.

To be straightforward about the limits: Corriden offers no payment for reports, makes no commitment about how quickly one will be answered, and is not in a position to promise you legal protection for testing. Those are real constraints, and pretending otherwise on a page about honesty in security would be self-defeating. General questions go to the same address — see contact.