Skip to content

Security verdict

20 checked, 4 not met, 21 declared, 0 not checked — 45 controls in the baseline.

Each control below is defined on the security baseline, which also says what each of the four states means. declared is not a pass.

Control What it checks State, scope and what is left
KYV-1 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. declared — 3 bearer-secret surfaces over 9 mint, compare and refuse sites (5 × mint, 2 × constant-time compare, 0 × compare, 2 × refusal); 2 further entries say what the derivation reached that is not a secret:
the HTTP Basic key on the api/ routes — core's own oc_api credential, read at system/library/api_gateway.php:850-881 from HTTP_AUTHORIZATION, REDIRECT_HTTP_AUTHORIZATION, PHP_AUTH_USER/PHP_AUTH_PW and apache_request_headers() in that order, and compared at api_gateway.php:948 with hash_equals against a dummy of equal length where the username is unknown. Compared in constant time at system/library/api_gateway.php:948. No refusal of an unset value sits in this extension's own code. Minted by core, so no entropy is asserted here: nothing in this baseline rests on core's own token helper.
the secret query parameter on extension/preorder/cron/preorder — minted at system/library/secret.php:43 as bin2hex(random_bytes(32)), 64 hexadecimal characters and 256 bits; compared at system/library/cron_access.php:104 with hash_equals, and refused at cron_access.php:94 when the stored secret is empty and at cron_access.php:100 when the parameter is not a string, which is what ?secret[]= arrives as. Compared in constant time at system/library/cron_access.php:104. Refused when unset at admin/controller/module/preorder.php:545, system/library/cron_access.php:94. Minted with 256 bits of CSPRNG output at system/library/secret.php:43, which meets the 128-bit floor.
the resume link — extension/preorder/preorder/preorder.pay and .method read get[code] and resolve it through preorder_order (catalog/controller/preorder/preorder.php:230). No comparison of it happens in this extension's own code. No refusal of an unset value sits in this extension's own code. Minted with 160 bits of CSPRNG output at admin/model/preorder/notify.php:642, which meets the 128-bit floor.
1.2.1 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. declared — machine-pass on the sinks: 0 sink sites asserted here, 0 not admitted; attested over the inventory: 38 store-derived subtrees over 10 templates; unverified beyond it: everything else:
module_preorder_status — setting. Rendered by admin/view/template/module/preorder.twig.
module_preorder_days_to_pay — setting. Rendered by admin/view/template/module/preorder.twig.
module_preorder_payment_mode — setting. Rendered by admin/view/template/module/preorder.twig.
payment_preorder_status — setting. Rendered by admin/view/template/payment/preorder.twig.
payment_preorder_sort_order — setting. Rendered by admin/view/template/payment/preorder.twig.
version_warning — the store's OpenCart release, through Compatibility. Rendered by admin/view/template/module/preorder.twig.
cron_scheduler_dead — the store's OpenCart release, through Compatibility — whether this release's own scheduler can run at all. Rendered by admin/view/template/module/preorder.twig.
sweep_url — setting — the stored sweep secret, in a URL built from HTTP_CATALOG and the scheduled route; empty on a store that has no secret. Rendered by admin/view/template/module/preorder.twig.
text_door_url — the same URL, inside a language string; the placeholder wording where there is no secret. Rendered by admin/view/template/module/preorder.twig.
text_door_cli — DIR_OPENCART — the store's own directory, inside a language string. Rendered by admin/view/template/module/preorder.twig.
rotate — a link to this screen's own rotate route, carrying the admin session token. Rendered by admin/view/template/module/preorder.twig.
payment_warning — whether the store has this extension's payment method installed and enabled. Rendered by admin/view/template/module/preorder.twig.
ready — database — orders carrying a pre-order line that is ready to be asked for, joined to core's oc_order. Rendered by admin/view/template/module/preorder.twig.
totals — database — five counts over preorder_order_product and core's oc_order on the admin screen, and core's own order total rows on the payment page. Rendered by admin/view/template/module/preorder.twig, catalog/view/template/preorder/preorder.twig.
preorders — database — order rows with customer name and e-mail out of core's oc_order. Rendered by admin/view/template/module/preorder.twig.
owed — database — one row per counter a live pre-order row watches: catalogue names and model, the row's own mode and date, and aggregate counts over preorder_order_product and core's stock columns. Rendered by admin/view/template/module/preorder.twig.
export — a link to this screen's own export route, carrying the admin session token. Rendered by admin/view/template/module/preorder.twig.
stale — database — the same rows, filtered to requests nobody answered. Rendered by admin/view/template/module/preorder.twig.
copy_panel — rendered partial over the merchant's own storefront wording, per language. Rendered by admin/view/template/module/preorder.twig.
copy — the merchant's stored wording overrides beside this extension's shipped defaults. Rendered by admin/view/template/module/copy_panel.twig.
languages — database — core's oc_language, unfiltered. Rendered by admin/view/template/module/copy_panel.twig.
language_readout — rendered partial over oc_language. Rendered by admin/view/template/module/preorder.twig.
resolution — rendered from oc_language, through LanguageReadout. Rendered by admin/view/template/module/language_readout.twig.
module — a link back to this extension's own settings route, carrying the admin session token. Rendered by admin/view/template/payment/preorder.twig.
default — database — the product-level pre-order row, or a zeroed default built from the store's configured payment mode. Rendered by admin/view/template/module/preorder_product_tab.twig.
option_values — database — core's option values for the product, each merged with this extension's row for it. Rendered by admin/view/template/module/preorder_product_tab.twig.
products — database — core's order product rows, with their option values. Rendered by catalog/view/template/preorder/preorder.twig.
payment_methods — the store's enabled payment extensions, as each one quoted for this order. Rendered by catalog/view/template/preorder/preorder.twig.
method — request — post[payment_method], narrowed to a method the store quoted. Rendered by catalog/view/template/preorder/preorder.twig.
cart — a link carrying the order's own get[code]. Rendered by catalog/view/template/preorder/preorder.twig.
language — setting — the storefront's config_language. Rendered by catalog/view/template/payment/preorder.twig.
error — a language string, chosen by the state of the order the code resolved to. Rendered by catalog/view/template/preorder/preorder.twig.
preorder — database — whether the product's own pre-order row resolves to a pre-order with nothing selected. Rendered by catalog/view/template/product/preorder.twig.
preorder_date — database — the row's expected date, into the merchant's own wording. Rendered by catalog/view/template/product/preorder.twig.
preorder_terms — the merchant's own wording for the payment mode the row carries. Rendered by catalog/view/template/product/preorder.twig.
preorder_json — database — every option value's state, date and wording, JSON-encoded and htmlspecialchars()-escaped in the controller because OpenCart runs Twig with autoescape off. Rendered by catalog/view/template/product/preorder.twig.
text_preorder_card — the merchant's own wording for a pre-ordered cart line. Rendered by catalog/view/template/product/preorder_card.twig.
api_panel — settings and database — whether the API is on, and core's own oc_api rows. Rendered by admin/view/template/module/api_panel.twig, admin/view/template/module/preorder.twig.
Unverified beyond it: the other 411 of 440 template expressions in 14 templates, and any $data subtree nobody enumerated. The inventory is an inventory and not a bound: completeness over the whole expression surface is unverifiable, so this residual is permanent, and it is published rather than left to be inferred from what is missing.
1.2.2 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. declared — 29 url attributes carrying a template expression, of 29 sink sites asserted:
Every url attribute in this extension's templates takes its value whole from the link helper.
1.2.3 No template expression is interpolated into a <script> element, so store data cannot end a string literal and start running. not met — 14 .twig files:
extensions/preorder/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> element
extensions/preorder/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> element
extensions/preorder/src/admin/view/template/module/preorder_product_tab.twig:90 — {{ state_inherit }} is interpolated inside a <script> element
extensions/preorder/src/catalog/view/template/payment/preorder.twig:10 — {{ language }} is interpolated inside a <script> element
extensions/preorder/src/catalog/view/template/preorder/preorder.twig:77 — {{ method }} is interpolated inside a <script> element
1.2.4 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. declared — 3 ways of building a statement, over 84 statements run and 42 values escaped:
escaped literal: every value this extension puts in a statement — Through $this->db->escape(), or an (int) cast where the value is numeric. The resume code is escaped the same way any other string is.
interpolated identifier: the table name in the installer's SHOW TABLES / SHOW COLUMNS / SHOW INDEX probes, and in Schema's CREATE TABLE statements — DB_PREFIX concatenated with a table name out of Schema's own constant list, at admin/model/module/preorder.php:264-305 and system/library/schema.php:234. Nothing from the request reaches it.
whole-clause concatenation: the dashboard's order listing — Dashboard::orders() takes a WHERE and an ORDER BY as whole strings (admin/model/preorder/dashboard.php:228-245). Both callers pass literals written in this file; no caller builds one from the request.
9 of the 84 statements are handed over already built, so what a rule reading the call site alone can see stops there; which mechanism built them is what the lines above say.
1.2.5 Nothing runs a command through the shell — no backtick, no exec() — so no value a store holds can become part of one. checked — 90 .php files
1.3.1 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. checked — 14 .twig files
1.3.2 Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided. not met — 90 .php files:
extensions/preorder/src/system/library/copy.php:392 — require runs a PHP file whose path is decided while running, which is code execution the source does not name
1.5.1 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. checked — 90 .php files
3.2.1 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. declared — 13 of 28 routes set a Content-Type of their own:
4 × application/json
1 × application/json, with no charset (customer/personal_data.php:230)
1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299)
1 × application/json, with no charset (customer/personal_data.php:359)
1 × application/json, with no charset (customer/purge.php:168)
1 × application/json, with no charset — the new sweep address, which is the one response in this extension that carries a live credential back to a browser
3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null
1 × none set, and no output written
1 × none — a 404 status line with a plain-text body on refusal, and nothing at all on a run
1 × none — it returns markup to core's checkout rather than writing a response
7 × none — nothing sets a Content-Type, so the store's default stands
4 × none — the whole screen is re-rendered, so the store's default stands
1 × none. The 404 body is plain text with no type set; an admitted run prints its counts to the terminal and writes its pass line to the diary
1 × text/csv; charset=utf-8, as an attachment named preorder-owed-.csv, with a Cache-Control of no-store
3.2.2 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. declared — 21 call sites in 6 templates, each declared with what it writes there:
5 × .append(
6 × .html(
8 × .prepend(
2 × innerHTML
3.3.1 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. checked — 90 .php files
3.4.2 A cross-origin header is a fixed value this code chose — never a wildcard, and never the origin the caller asked for. checked — 90 .php files
3.5.1 Every route that changes something says what stands between it and a request another website caused a visitor's browser to make. declared — 8 routes of 28 reaches a model write; the 7 admin ones among them stand behind the user_token core checks before dispatch, and 18 admin routes are gated that way in all:
extensions/preorder/src/catalog/controller/payment/preorder.php:70 — extension/preorder/payment/preorder.confirm reaches a model write and stands behind the storefront session cookie alone; OpenCart carries no anti-CSRF token on the catalog side for it to check.
3.5.2 No route grants a cross-origin caller anything, so nothing here is left depending on a browser's preflight to refuse one. checked — 90 .php files
3.5.3 A route that writes refuses a request that is not a POST, so a link somebody follows cannot make the change on their behalf. not met — 90 .php files:
extensions/preorder/src/admin/controller/customer/personal_data.php:321 — PersonalData::grant() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/customer/purge.php:147 — Purge::remove() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/module/preorder.php:722 — Preorder::rotate() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/module/preorder.php:762 — Preorder::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/module/preorder.php:851 — Preorder::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/module/preorder.php:1091 — Preorder::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/admin/controller/payment/preorder.php:86 — Preorder::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/preorder/src/catalog/controller/payment/preorder.php:70 — Preorder::confirm() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
4.1.1 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. declared — 13 of 28 routes set a Content-Type of their own:
4 × application/json
1 × application/json, with no charset (customer/personal_data.php:230)
1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299)
1 × application/json, with no charset (customer/personal_data.php:359)
1 × application/json, with no charset (customer/purge.php:168)
1 × application/json, with no charset — the new sweep address, which is the one response in this extension that carries a live credential back to a browser
3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null
1 × none set, and no output written
1 × none — a 404 status line with a plain-text body on refusal, and nothing at all on a run
1 × none — it returns markup to core's checkout rather than writing a response
7 × none — nothing sets a Content-Type, so the store's default stands
4 × none — the whole screen is re-rendered, so the store's default stands
1 × none. The 404 body is plain text with no type set; an admitted run prints its counts to the terminal and writes its pass line to the diary
1 × text/csv; charset=utf-8, as an attachment named preorder-owed-.csv, with a Cache-Control of no-store
5.2.1 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. declared — 0 upload surfaces across 28 routes
5.2.2 An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands. declared — 0 upload surfaces across 28 routes
5.3.1 Every file this extension writes says whether a browser can fetch it, and nothing it writes where a browser can reach is program code. declared — 11 write sites, 3 of them fetchable by a browser:
system/library/diary.php:437 — kyvero.log in the store's own log directory — the DIR_LOGS this class is handed, with no part of the name coming from a request — one record appended per write, at system/library/diary.php:437
system/library/diary.php:470 — the same kyvero.log, opened r+ to trim it back under the 1 MiB cap, at system/library/diary.php:470
system/library/diary.php:495 — the same kyvero.log, rewritten to what a trim kept — oldest-first, on a line boundary, under an exclusive non-blocking lock — at system/library/diary.php:495
5.3.2 Every path this extension writes to is written down beside the code, with where the name in it came from. declared — 11 write sites, each declared with its file:line and pinned against the token stream both ways:
1 × a memory stream, opened per export and discarded with the request
3 × log file
6 × stream
1 × the Owed by product rows: ids, model, catalogue names, mode, date and counts, quoted by fputcsv()
6.2.6 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. checked — 14 .twig files
6.2.7 A masked field does not refuse a paste or shut a password manager out of it. checked — 14 .twig files
6.3.2 No credential is written into the source — no default account, and no password or key a reader of the shipped files could use. checked — 90 .php files
8.1.1 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. declared — 28 routes: 18 admin, 10 catalog, each declared beside the code
8.2.1 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. declared — 18 admin routes: 10 pin a permission themselves, 4 at one same-class hop, 0 at two (the hop ceiling), 4 unpinned:
extensions/preorder/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/preorder/customer/purge before dispatch. No model write is reachable from it.
extensions/preorder/src/admin/controller/module/preorder.php:272 — Preorder::index() pins no permission of its own; core checks access on extension/preorder/module/preorder before dispatch. No model write is reachable from it.
extensions/preorder/src/admin/controller/module/preorder.php:359 — Preorder::export() pins no permission of its own; core checks access on extension/preorder/module/preorder before dispatch. No model write is reachable from it.
extensions/preorder/src/admin/controller/payment/preorder.php:41 — Preorder::index() pins no permission of its own; core checks access on extension/preorder/payment/preorder before dispatch. No model write is reachable from it.
8.2.2 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. declared — 13 triples over 9 of 10 catalog routes; the admin half is one line on the shared page:
extension/preorder/api/gateway.fail — a holder of one of core's oc_api credentials: Absent, unknown, duplicated, disabled, wrong-keyed and address-not-listed are one refusal with one code and one message, so the envelope partitions nothing. Selected by none — not a record; this route renders a refusal and reads nothing
extension/preorder/api/v1/line — a holder of one of core's oc_api credentials: The credential cannot be scoped. A line carries a product, an order id and dates — no name, address or amount, which are core's, on core's order — and option_key is never selected. Selected by line_id
extension/preorder/api/v1/line — a holder of one of core's oc_api credentials, walking the collection: A filter is a convenience for a caller who wants a narrower read, not a boundary: the same credential may simply leave it off. Selected by none — not a record; the filters and the cursor are a page of a walk rather than a record selector
extension/preorder/api/v1/payment — a holder of one of core's oc_api credentials: The credential cannot be scoped, which is why the payment link's token is never selected: whoever holds it can pay for the order, and a caller holding every one would hold every order's checkout. Selected by payment_id
extension/preorder/api/v1/payment — a holder of one of core's oc_api credentials, walking the collection: A filter is a convenience for a caller who wants a narrower read, not a boundary: the same credential may simply leave it off. Selected by none — not a record; the filters and the cursor are a page of a walk rather than a record selector
extension/preorder/cron/preorder — OpenCart's own scheduler: One pass over every outstanding payment request on the installation, which is what a store-wide schedule is. Selected by none — not a record; $cron_id names the oc_cron row that dispatched, never a row this extension reads
extension/preorder/cron/preorder — whoever holds the sweep secret: The same pass the scheduler drives, for a merchant whose host gives them a cron panel and no shell. It partitions nothing per identity because there is one identity: the store's own schedule. Selected by none — not a record; get[secret] is the whole credential and addresses nothing
extension/preorder/cli/preorder — whoever can run PHP on the server: Reached through extension/preorder/preorder.php, and refused outright to anything that is not a terminal. It drives the same model method the scheduled route drives and does not reach through that route. Selected by none — not a record, and the command takes no arguments at all: anything after the filename is refused
extension/preorder/payment/preorder — the shopper's own session: It renders one button out of a language string and the store's language code. Selected by none — not a record; the method reads no request key at all
extension/preorder/payment/preorder.confirm — the shopper's own session: A shopper can confirm the order their own checkout put in their session, and no other. Selected by none — not a record; the order is taken from $this->session->data['order_id'] and never from the request
extension/preorder/preorder/preorder.pay — whoever holds the emailed link, signed in or not: One order per code, and the code is the whole of the authority — there is no second check that the reader is the customer. Selected by get[code]
extension/preorder/preorder/preorder.method — whoever holds the emailed link, signed in or not: One order per code, and the code is the whole of the authority — there is no second check that the reader is the customer. Selected by get[code]
extension/preorder/preorder/preorder.method — whoever holds the emailed link, signed in or not: The method has to be one the store quoted for this order; it addresses nothing of anybody else's. Selected by none — not a record; post[payment_method] names a payment extension rather than selecting a row
8.3.1 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. declared — 10 distinct bounds, each named by the triple it scopes:
bounded by nothing — an OpenCart API user opens every store in the installation, and this resource does not narrow that
bounded by nothing is read and nothing is written
bounded by nothing — a filter is the caller narrowing their own read, never the store narrowing what they may see
bounded by nothing — the refusal is the same for every caller
bounded by preorder_order.code, a 160-bit oc_token(40) minted per order and replaced on every reissue
bounded by the framework: the front controller spreads an empty $args on every release this extension names, so a query string cannot become a method argument
bounded by the one secret this installation stores, minted at install and replaced only by Rotate
bounded by the operating-system account the file is run as
bounded by the session, which only the shopper's own checkout writes
bounded by the store's own enabled payment methods for that order, which is the list the page offered
9.1.1 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. declared — 1 self-contained surface of 3 bearer-secret surfaces:
admin/controller/module/preorder.php:545 — yes — one secret per installation is the whole authority for the URL door; it names no record and there is no second factor. The other caller admitted at that route proves itself with the $cron_id argument core's scheduler passes and carries no secret at all, and the command line door proves itself with the server API
9.1.2 Every hashing algorithm is a literal in the source, from a fixed allowlist, so nothing arriving in a request can choose a weaker one. checked — 90 .php files
9.1.3 The key a signed secret is checked against comes from somewhere this extension was configured with, never from anything inside the secret itself. declared — 3 bearer-secret surfaces, from core, minted — never from anything inside the secret presented:
system/library/api_gateway.php:948 — core
admin/controller/module/preorder.php:545 — minted
admin/model/preorder/notify.php:642 — minted — oc_token(40) at admin/model/preorder/notify.php:642, which is 40 hex characters and so 160 bits, replaced on every reissue
9.2.1 A secret that carries its own expiry is accepted only inside it, and the declaration says which ones carry one. declared — 1 surface of 3 bearer-secret surfaces could carry a validity span inside the secret itself; the rest are a reference to a row, whose expiry is a column on it rather than a claim the caller presents:
admin/controller/module/preorder.php:545 — yes — one secret per installation is the whole authority for the URL door; it names no record and there is no second factor. The other caller admitted at that route proves itself with the $cron_id argument core's scheduler passes and carries no secret at all, and the command line door proves itself with the server API
11.3.1 Nothing encrypts with a broken mode or padding — no ECB, no PKCS#1 v1.5. checked — 90 .php files
11.3.2 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. checked — 90 .php files
11.4.1 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. declared — 1 hash use over 1 call to 1 hash function:
md5: the key an order line is matched on while an order is rebuilt — Rebuild::key() at system/library/rebuild.php:84 hashes the sorted option value ids of one line so that two lines carrying the same options collapse onto one key. It is an identity for a row inside one request and nothing else: it guards nothing, it is never compared against anything a caller presented, and it is never stored.
12.1.1 No outbound request asks for a TLS version below 1.2, and none pins itself to one at all. checked — 90 .php files
12.2.1 An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. checked — 90 .php files
12.2.2 An outbound request trusts your server's own certificate store: nothing here bundles a certificate authority of its own or turns verification off. checked — 90 .php files
14.2.1 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. not met — 2 bearer-secret surfaces of 3 travel in a URL:
admin/controller/module/preorder.php:545 — whatever scheme the store's catalog is served on. The secret travels in a query string a merchant pastes into a host's cron panel or a cron service, so it also sits in that panel, in the admin screen that prints it (admin/controller/module/preorder.php:545, guarded by $secret !== '' so a store with none prints no address at all), in the JSON that rotate answers with, and in the web server's access log
admin/model/preorder/notify.php:642 — the URL query string of a link emailed to the customer, over whatever scheme the store's own site_url sets
14.3.1 Nothing is left behind in the browser's own storage for the next person at that computer to read. checked — 14 .twig files
15.2.1 The extension bundles no third-party library, so there is nothing inside it for you to keep patched other than our own code. checked — 90 .php files
15.3.1 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. declared — 38 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:
module_preorder_status — setting
module_preorder_days_to_pay — setting
module_preorder_payment_mode — setting
payment_preorder_status — setting
payment_preorder_sort_order — setting
version_warning — the store's OpenCart release, through Compatibility
cron_scheduler_dead — the store's OpenCart release, through Compatibility — whether this release's own scheduler can run at all
sweep_url — setting — the stored sweep secret, in a URL built from HTTP_CATALOG and the scheduled route; empty on a store that has no secret
text_door_url — the same URL, inside a language string; the placeholder wording where there is no secret
text_door_cli — DIR_OPENCART — the store's own directory, inside a language string
rotate — a link to this screen's own rotate route, carrying the admin session token
payment_warning — whether the store has this extension's payment method installed and enabled
ready — database — orders carrying a pre-order line that is ready to be asked for, joined to core's oc_order
totals — database — five counts over preorder_order_product and core's oc_order on the admin screen, and core's own order total rows on the payment page
preorders — database — order rows with customer name and e-mail out of core's oc_order
owed — database — one row per counter a live pre-order row watches: catalogue names and model, the row's own mode and date, and aggregate counts over preorder_order_product and core's stock columns
export — a link to this screen's own export route, carrying the admin session token
stale — database — the same rows, filtered to requests nobody answered
copy_panel — rendered partial over the merchant's own storefront wording, per language
copy — the merchant's stored wording overrides beside this extension's shipped defaults
languages — database — core's oc_language, unfiltered
language_readout — rendered partial over oc_language
resolution — rendered from oc_language, through LanguageReadout
module — a link back to this extension's own settings route, carrying the admin session token
default — database — the product-level pre-order row, or a zeroed default built from the store's configured payment mode
option_values — database — core's option values for the product, each merged with this extension's row for it
products — database — core's order product rows, with their option values
payment_methods — the store's enabled payment extensions, as each one quoted for this order
method — request — post[payment_method], narrowed to a method the store quoted
cart — a link carrying the order's own get[code]
language — setting — the storefront's config_language
error — a language string, chosen by the state of the order the code resolved to
preorder — database — whether the product's own pre-order row resolves to a pre-order with nothing selected
preorder_date — database — the row's expected date, into the merchant's own wording
preorder_terms — the merchant's own wording for the payment mode the row carries
preorder_json — database — every option value's state, date and wording, JSON-encoded and htmlspecialchars()-escaped in the controller because OpenCart runs Twig with autoescape off
text_preorder_card — the merchant's own wording for a pre-ordered cart line
api_panel — settings and database — whether the API is on, and core's own oc_api rows
16.2.5 No log line names a credential — no token, secret, signature or password is written into the file the error log screen renders. checked — 90 .php files
16.4.1 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. checked — 90 .php files
16.5.1 No error message carrying internal detail — a database driver puts the failing statement in one — is thrown onward or rendered to a response. checked — 90 .php files