Skip to content

The security baseline

Every extension in this repository is checked against one published list of numbered security controls. Each extension's result against that list is its verdict, and every verdict is on this site, linked from the roster below.

The claim attached to it is narrow on purpose. It is not we passed a review. It is: here is what we check, here is what we found, and you can run it again yourself against source we ship unobfuscated.

The controls, and what a result means

Most of the list is ASVS 5.0.0 by number. One control is ours: KYV-1, on the secrets a caller presents. It is admitted only because no ASVS requirement at any level covers that surface, there is a live one here, and the same machinery can assert it. Its reasoning is borrowed from the standard by attribution rather than by printing a requirement number it does not implement. Each control is stated below in our own words rather than the standard's, so that the table can be read by somebody who has never opened it.

A control is asserted in one of two modes. A machine control is a rule over source text, at a set of sites that can be enumerated, that has run across every extension without a single false positive (not "few", because a rule needing a suppression list has already failed). Everything else is declared: the extension writes down the object the control is about, that declaration ships beside the code, and it renders onto the verdict page.

declared is not a pass. It says what a thing is: which routes exist, which values reach a template, where a file gets written. It does not say the thing is fine, and a page of declared rows is not a page of passes. It is worth something anyway: a declaration nobody can check is prose, and this one is published beside the code it describes, where a reader can disagree with it.

State and mode are different axes, so a declared control can still publish not met. 14.2.1 does, everywhere a secret travels in a URL: the rule for it is hard, and a hard rule is not a reason to silently publish not checked instead of the red somebody designed.

One control is carried by two arms, and it is worth spelling out, because from a distance it looks like the suppression list a machine control is not allowed to have. 16.4.1 asks that everything written to the store's error log is escaped first. Most of what an extension writes now goes through its own diagnostic writer, and that writer escapes every message inside itself. At those call sites the control is discharged not by reading the call site but by the check that every copy of the writer is byte-for-byte identical to the single canonical file in this repository. A direct write to the error log by anything that is not that writer is still asserted exactly as it always was: the escape has to be the whole of the argument, literals included.

The difference from a suppression is not a wording preference. A suppression says this site is exempt. This says this site's guarantee is carried by a different check: a check named here, which fails the build the moment it stops holding. Change one character in any copy of the writer and make check goes red.

What each state means

State What it says about an extension
checked A machine rule ran over an enumerable set of sites and found nothing. Published with its count, because a count of zero says "vacuously true" better than a fifth state would.
not met The control is failed. Published with file:line rather than a count, so that a defect cannot move from one site to another without this page changing.
declared Not a pass. The extension has declared the object the control is about, and that declaration ships beside the code and renders onto this page. It says what the thing is; it does not say the thing is fine.
not checked Nothing qualifies. A control number no scanner arms fails the build, so this state exists in the vocabulary in order to be unreachable.

The controls

45 controls: 41 ASVS 5.0.0 Level 1 requirements, 3 adopted by number at Level 2, and 1 house control.

ASVS Level 1 is 70 requirements in all, and they divide four ways. 41 are the extension's and are the 41 Level 1 controls above. 18 are OpenCart's or your server's — the login screen, the session, the TLS certificate. 6 have no surface in an extension at all. And 5 are ours and are left out anyway, because what they ask about is a judgement that no rule over source and no declaration can carry without a list of exceptions. That last number is here rather than rounded away: a list nobody drew a boundary around is not a list.

Level 1 asks for no logging at all, which is why the three Level 2 adoptions are the log and error controls, and every other item in that chapter stays out. An extension does own a log format, a location and a retention rule — it writes a diagnostic file of its own beside your store's error log, and caps it at 1 MiB by dropping its oldest lines. What it owns is a support artefact rather than an audit trail: nothing is kept for detection, nothing is protected from the store owner who can download and clear it in one click, and nothing is sent anywhere. The rest of the chapter asks about integrity, retention and transport, so adopting it here would publish a promise we deliberately do not make. What that file is held to instead is a convention of ours, published beside it rather than as a row in this table.

23 machine, 22 declared, 0 not checked — the mode mix of the baseline as it stands.

Control Provenance Mode What it checks
KYV-1 house declared A secret an untrusted caller presents is compared in constant time and refused when it is unset — and where this extension mints it rather than taking core's or a merchant's, it carries at least 128 bits from a cryptographic random source.
1.2.1 asvs-l1 declared Store data meets markup safely where the danger is decidable — an unquoted attribute, a URL the template composed itself, a style, hand-built XML — and every store-derived subtree a template of this extension renders is written down beside the code. Beyond those two, nothing is claimed, and the page says so.
1.2.2 asvs-l1 declared A URL a template builds for itself, rather than taking one whole from the link helper, has every value in it URL-encoded — so nothing a store holds can add a parameter of its own or change where the link goes.
1.2.3 asvs-l1 machine No template expression is interpolated into a <script> element, so store data cannot end a string literal and start running.
1.2.4 asvs-l1 declared Every way this extension builds a database statement is written down beside the code, so how a value reaches a query is a published answer rather than something to go looking for.
1.2.5 asvs-l1 machine Nothing runs a command through the shell — no backtick, no exec() — so no value a store holds can become part of one.
1.3.1 asvs-l1 machine No screen binds a rich-text editor whose HTML this extension would then render back out, because nothing here sanitises HTML and no sanitiser ships with it.
1.3.2 asvs-l1 machine Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided.
1.5.1 asvs-l1 machine Every XML parser is left at the restrictive default: nothing turns on external entity resolution, which is what would turn reading a spreadsheet into reading your server's files.
3.2.1 asvs-l1 declared Every route declares the response type it sets, as the code sets it, so nothing is left for a browser to re-interpret as something it is not.
3.2.2 asvs-l1 declared Every place a script hands a value to the page as markup rather than as text is written down beside the code, with what it puts there.
3.3.1 asvs-l1 machine A cookie this extension sets carries the Secure attribute at the call that sets it, so a browser cannot send it back over plain HTTP.
3.4.2 asvs-l1 machine A cross-origin header is a fixed value this code chose — never a wildcard, and never the origin the caller asked for.
3.5.1 asvs-l1 declared Every route that changes something says what stands between it and a request another website caused a visitor's browser to make.
3.5.2 asvs-l1 machine No route grants a cross-origin caller anything, so nothing here is left depending on a browser's preflight to refuse one.
3.5.3 asvs-l1 machine A route that writes refuses a request that is not a POST, so a link somebody follows cannot make the change on their behalf.
4.1.1 asvs-l1 declared A response carrying a body says what that body is, and the route table records the Content-Type each route sets rather than the one it ought to.
5.2.1 asvs-l1 declared An upload is accepted on the server's terms — what the bytes are, not what the caller said they were — and every surface that takes one is declared.
5.2.2 asvs-l1 declared An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands.
5.3.1 asvs-l1 declared Every file this extension writes says whether a browser can fetch it, and nothing it writes where a browser can reach is program code.
5.3.2 asvs-l1 declared Every path this extension writes to is written down beside the code, with where the name in it came from.
6.2.6 asvs-l1 machine A field that takes a password or a key is masked, so it is not left readable on the screen or in a screenshot of it.
6.2.7 asvs-l1 machine A masked field does not refuse a paste or shut a password manager out of it.
6.3.2 asvs-l1 machine No credential is written into the source — no default account, and no password or key a reader of the shipped files could use.
8.1.1 asvs-l1 declared Every route the extension answers is written down beside the code, with what guards it — and the gate refuses a route nobody wrote down and a written-down route nothing answers.
8.2.1 asvs-l1 declared An admin route that changes something tests the permission itself, in a condition that can refuse — and a route that only reads says so, standing behind the check OpenCart makes before dispatch.
8.2.2 asvs-l1 declared A storefront route that reaches a record says which caller may reach which records, and what selects one — so reaching somebody else's is a question with a written answer.
8.3.1 asvs-l1 declared What bounds a caller to their own records comes from the server — a session, a stored row, the store id — and never from a value the caller supplied.
9.1.1 asvs-l1 declared A secret that carries its own claim — an identity inside the string rather than a row to look up — is only believed after the signature beside it has been checked.
9.1.2 asvs-l1 machine Every hashing algorithm is a literal in the source, from a fixed allowlist, so nothing arriving in a request can choose a weaker one.
9.1.3 asvs-l1 declared The key a signed secret is checked against comes from somewhere this extension was configured with, never from anything inside the secret itself.
9.2.1 asvs-l1 declared A secret that carries its own expiry is accepted only inside it, and the declaration says which ones carry one.
11.3.1 asvs-l1 machine Nothing encrypts with a broken mode or padding — no ECB, no PKCS#1 v1.5.
11.3.2 asvs-l1 machine Where anything is encrypted, the cipher is a literal in the source from a short allowlist, so nothing arriving in a request can choose a weaker one.
11.4.1 asvs-l1 declared Every hash this extension computes is written down with what it is for, so a hash naming a cache entry is not read as one standing in front of a secret.
12.1.1 asvs-l1 machine No outbound request asks for a TLS version below 1.2, and none pins itself to one at all.
12.2.1 asvs-l1 machine An outbound request is made over TLS with the certificate verified, and never falls back to cleartext.
12.2.2 asvs-l1 machine An outbound request trusts your server's own certificate store: nothing here bundles a certificate authority of its own or turns verification off.
14.2.1 asvs-l1 declared A credential is not carried in a URL, where a browser history, a referrer header and a proxy log each keep their own copy of it.
14.3.1 asvs-l1 machine Nothing is left behind in the browser's own storage for the next person at that computer to read.
15.2.1 asvs-l1 machine The extension bundles no third-party library, so there is nothing inside it for you to keep patched other than our own code.
15.3.1 asvs-l1 declared What reaches a page is an enumerated set of values rather than whole database rows handed over wholesale, and every one of them is written down.
16.2.5 asvs-l2 machine No log line names a credential — no token, secret, signature or password is written into the file the error log screen renders.
16.4.1 asvs-l2 machine Everything written to the error log is escaped first, so nothing a store holds can forge a record or close the box a merchant reads the log in.
16.5.1 asvs-l2 machine No error message carrying internal detail — a database driver puts the failing statement in one — is thrown onward or rendered to a response.

Scoping, on the admin side

No admin route in this repository partitions records per administrator, so scoping is declared on catalog routes only and 8.2.2 and 8.3.1 read nothing on the admin side. The support is a count rather than a claim: 44 $this->user->getId() sites in shipped code, every one of them attribution (who did this, written onto a history row or a diary line) and none a filter on what an administrator may see, and 0 occurrences of user_group_id anywhere at all.

Every extension's verdict

An extension appears here because it is in the repository, not because it passed. hello_world is the empty scaffold we test the tooling against; it is checked like the rest, and it is the one extension with no documentation section for a verdict page to live in, so its result is published here and nowhere else.

Extension Controls not met Its verdict
Abandoned Cart Recovery 4 Verdict
Advanced Product Filters 2 Verdict
B2B Pricing 3 Verdict
Back In Stock 5 Verdict
Delivery Date 4 Verdict
Gift Cards 2 Verdict
Hello World 1 Gated; no documentation section to publish it in
Import/export 7 Verdict
Loyalty 3 Verdict
Pre-Order 4 Verdict
Product Bundles 4 Verdict
Product Configurator 2 Verdict
Product Feed 5 Verdict
Product Search 2 Verdict
Profitability Copilot 2 Verdict
Returns Portal 4 Verdict
Review Requests 5 Verdict
SEO Suite Pro 4 Verdict
Wishlist 7 Verdict

A verdict describes the source in this repository as it stands. Nothing in it is a claim about a particular release: the page is generated from that source, committed beside it, and checked on every build, so a result that has stopped being true fails the build rather than sitting here. The one thing that follows from that, and is worth saying plainly, is that the source runs ahead of the archive you downloaded. A control that reads green here may still be red in the copy installed on your store, until the next release of that extension carries the fix to you.

When a control here is red and you have already bought

It is published anyway, and that is the whole policy. A verdict page names the control, the file and the line, on an extension you can buy today. There is no held-back address, no row silently left out and no fifth state that means red, but not in public.

Part of that is mechanism rather than nerve. These pages are generated from the source, committed beside it and checked on every build, so leaving a red off one would fail the build rather than tidy the page. And part of it is that the file:line is the point: a count alone would let a defect move from one place to another without this page changing, and a page that overstates what we found is itself something we would want reported.

We do ask a reporter to wait ninety days, and we do not wait ourselves. That is not two standards. Somebody reporting a flaw in our code publishes about software they cannot fix, so the wait is what gives the people running it a fix to move to. We publish about our own code, and the fix is ours to ship.

  • What closes the window is a patch release, never silence. A red goes green when a release carries the fix to your store.
  • A listing is pulled only as a stop-gap, and only for a red that an unauthenticated caller can reach remotely while the marketplace is still reviewing the fix. That trigger is a fact about the archive on sale rather than about a clock, so it adds no deadline of its own.

When a red goes green

The changelog line names the control number. A changelog is otherwise written in terms of what a release means for you, and that is exactly the register in which a security fix becomes "improved logging". So when an extension stops failing a control, its Changelog page says which one, by number, and you can match it against the row you read here.

That line is also the only per-release drift note you will find. Every extension on sale has source commits since its last tag (the ordinary state of anything still being worked on), so a note saying so on each page would be wallpaper. What is worth reading is the moment a control's answer changed, and that is what the changelog line says.

What can be taken off this list

Nothing is removed silently. Removing a control gets the same treatment as breaking an API promise: the list is a published promise, and a shorter one is a smaller promise, so it is argued for in the open rather than edited in.

Adding one gets nothing. A new control that turns an extension you already own red ships on its own, on the day it is written, and is never held back until the thing it found is fixed. An unmet control gets a published verdict, not a removal. That is the only way the list can grow without the growing being shaped by what it would say.

What this does not cover

  • OpenCart itself, and other vendors' extensions. We check our own code. A store runs far more than that, and none of the rest is ours to verify.
  • Your server. PHP version, file permissions, TLS, the admin password and who has it are yours.
  • Anything outside the list. The controls are the controls. A defect nobody wrote a control for is a defect the baseline has nothing to say about, and saying so is cheaper than implying the list is exhaustive.
  • A count of severity. No control carries a rating here. What a not met row costs your store depends on your store.

If you find something, support is the route in, and we would rather hear it than not.