Security verdict¶
20 checked, 5 not met, 20 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 — 2 bearer-secret surfaces over 6 mint, compare and refuse sites (3 × mint, 1 × constant-time compare, 0 × compare, 2 × refusal); 2 further entries say what the derivation reached that is not a secret: the token on extension/review_requests/review_requests/review_requests and on its .submit and .unsubscribe methods — minted at catalog/model/review_requests/pass.php:1257 as oc_token(40), which is 40 hexadecimal characters and therefore 160 bits, not 40 bytes; read at catalog/controller/review_requests/review_requests.php:527 from the POST body first and the query string second; refused as blank at catalog/model/review_requests/ask.php:64 and at pass.php:1253 before any lookup. No comparison of it happens in this extension's own code. Refused when unset at catalog/model/review_requests/ask.php:64, catalog/model/review_requests/pass.php:1253. Minted with 160 bits of CSPRNG output at catalog/model/review_requests/pass.php:1257, which meets the 128-bit floor.the HTTP Basic key on the three 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. |
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: 47 store-derived subtrees over 8 templates; unverified beyond it: everything else:action — request — the unsubscribe form's own action, with the bearer token in the query string. Rendered by catalog/view/template/review_requests/review_requests.twig, catalog/view/template/review_requests/unsubscribe.twig.address — settings — config_name and config_address for the store the ask belongs to. Rendered by catalog/view/template/mail/request.twig.all_done — database — whether every line of the order has been reviewed. Rendered by catalog/view/template/review_requests/review_requests.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/review_requests.twig.ask — settings — the merchant's own wording blob for this store, with {store}, {name} and {date} filled in. Rendered by catalog/view/template/mail/request.twig.backfill — settings — the configured backfill window. Rendered by admin/view/template/module/review_requests_backfill.twig.backfill_modal — a rendered sub-template of this extension's own, carrying the subtrees above. Rendered by admin/view/template/module/review_requests.twig.behind — database — how many orders are waiting to be asked. Rendered by admin/view/template/module/review_requests.twig.cards — database — the order's lines: product names, images, and any review already written against them. Rendered by catalog/view/template/mail/request.twig, catalog/view/template/review_requests/review_requests.twig.checklist — settings and database — the readiness lines, each with what is wrong with it. Rendered by admin/view/template/module/review_requests.twig.code — database — the language code the readout is being written against. Rendered by admin/view/template/module/review_requests.twig.copy_panel — settings and database — the merchant's own wording, per store and per language. Rendered by admin/view/template/module/copy_panel.twig, admin/view/template/module/review_requests.twig.filter_email — request — what the merchant typed into the requests search. Rendered by admin/view/template/module/review_requests.twig.greeting — settings — the merchant's own wording blob, with {store}, {name} and {date} filled in. Rendered by catalog/view/template/mail/request.twig.intro — settings — the merchant's own landing-page wording. Rendered by catalog/view/template/review_requests/review_requests.twig.language — settings — the language code the page or the email is being rendered in. Rendered by admin/view/template/module/copy_panel.twig, catalog/view/template/mail/request.twig.language_readout — database — the store's installed languages against what this extension ships. Rendered by admin/view/template/module/language_readout.twig, admin/view/template/module/review_requests.twig.link — database — the landing-page address for this ask, with the bearer token in the query string. Rendered by catalog/view/template/mail/request.twig.logo — settings — config_logo for the store the ask belongs to, linked at its original size. Rendered by catalog/view/template/mail/request.twig.moderation — settings — the merchant's own wording about approval. Rendered by catalog/view/template/mail/request.twig, catalog/view/template/review_requests/review_requests.twig.more — database — the product names of the lines beyond the three carded. Rendered by catalog/view/template/mail/request.twig.notice — session — what the last post-redirect-get act left to say. Rendered by admin/view/template/module/review_requests.twig.notice_error — session — whether that notice is a failure. Rendered by admin/view/template/module/review_requests.twig.order_statuses — database — core's own order statuses. Rendered by admin/view/template/module/review_requests.twig.passes — database — the recent pass records, with the error text of any that broke. Rendered by admin/view/template/module/review_requests.twig.pills — database — a count per request state. Rendered by admin/view/template/module/review_requests.twig.ready — settings and database — whether every checklist line passes. Rendered by admin/view/template/module/review_requests.twig.reason — settings — the shipped reason sentence with the store name and the customer first name filled in. Rendered by catalog/view/template/mail/request.twig.request_filter — request — the order id, address and product the merchant filtered on. Rendered by admin/view/template/module/review_requests.twig.request_filtered — request — whether any of those filters is set. Rendered by admin/view/template/module/review_requests.twig.request_stores — database — the store names the requests list is grouped by. Rendered by admin/view/template/module/review_requests.twig.requests — database — the ask rows: customer addresses, order ids, states, and the transport error of any that failed. Rendered by admin/view/template/module/review_requests.twig.state — database — whether this address is on the suppression list. Rendered by catalog/view/template/review_requests/unsubscribe.twig.store — settings — config_name for the store the ask belongs to. Rendered by catalog/view/template/mail/request.twig.store_id — request and database — which store the screen is working in. Rendered by admin/view/template/module/review_requests.twig, admin/view/template/module/review_requests_backfill.twig.store_url — settings — the base address of the store the ask belongs to. Rendered by catalog/view/template/mail/request.twig.stores — database and settings — the store names, config_name standing in for store 0. Rendered by admin/view/template/module/review_requests.twig.suppressions — database — the suppression rows, which are customer addresses. Rendered by admin/view/template/module/review_requests.twig.sync_notice — settings and runtime — whether the note is due on this OpenCart version and whether the merchant put it away. Rendered by admin/view/template/module/review_requests.twig.tab — request — which tab of the screen is open. Rendered by admin/view/template/module/review_requests.twig.text_ask — language and database — the shipped sentence with the store name and the customer's own address in it. Rendered by catalog/view/template/review_requests/unsubscribe.twig.text_stopped — language and database — the shipped sentence with the store name and the customer's own address in it. Rendered by catalog/view/template/review_requests/unsubscribe.twig.text_unsubscribe — settings — the merchant's own wording for the footer link. Rendered by catalog/view/template/mail/request.twig.title — settings — the merchant's own subject line, with {store}, {name} and {date} filled in. Rendered by catalog/view/template/mail/request.twig.token — request — the bearer token as it arrived, re-emitted into the form and the links. Rendered by catalog/view/template/review_requests/review_requests.twig, catalog/view/template/review_requests/unsubscribe.twig.unsubscribe — database — the unsubscribe address for this ask, with the bearer token in the query string. Rendered by catalog/view/template/mail/request.twig.user_token — session — core's own admin session token. Rendered by admin/view/template/module/review_requests.twig.Unverified beyond it: the other 528 of 572 template expressions in 12 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. | not met — 44 url attributes carrying a template expression, of 44 sink sites asserted:extensions/review_requests/src/admin/view/template/module/review_requests.twig:138 — href="#request-{{ request.ask_id }}" composes a URL in the template rather than taking one whole from the link helper, and not every value in it is URL-encoded |
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 — 12 .twig files:extensions/review_requests/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:672 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:675 — {{ picker.route }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:675 — {{ user_token }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:681 — {{ picker.id }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:688 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:690 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:692 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:693 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:697 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/admin/view/template/module/review_requests.twig:701 — {{ picker.key }} is interpolated inside a <script> elementextensions/review_requests/src/catalog/view/template/review_requests/review_requests.twig:135 — {{ minimum }} is interpolated inside a <script> elementextensions/review_requests/src/catalog/view/template/review_requests/review_requests.twig:137 — {{ text_counter|json_encode|raw }} is interpolated inside a <script> elementextensions/review_requests/src/catalog/view/template/review_requests/review_requests.twig:137 — {{ text_enough|json_encode|raw }} 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 — 4 ways of building a statement, over 123 statements run and 49 values escaped:a value interpolated into a statement as an escaped string literal — $this->db->escape($value) inside single quotes, which is core's own idiom. Twenty-two sites in catalog/model/review_requests/pass.php, nine in catalog/model/api/review_requests.php, eight in admin/model/module/review_requests.php, five in catalog/model/review_requests/ask.php, and one each in the settings mirror, the API event handler and system/library/template_source.php.a value interpolated into a statement as a bare integer — A PHP (int) cast concatenated without quotes — used for ask ids, line ids, order ids and store ids throughout the four models.a table name interpolated as an identifier — DB_PREFIX concatenated between backticks. It comes from the store's config.php and never from a request.a whole clause concatenated onto a statement — A WHERE built from the screen's or the API's filters, and an IN (...) list built by implode() over ids each of which has been through intval() first — admin/model/module/review_requests.php:779, catalog/model/review_requests/pass.php:1571 and catalog/model/api/review_requests.php:430. The two exclusion lists at catalog/model/review_requests/pass.php:1841 and :1852 are the same shape, over ids that have been through Configuration::ids(), which keeps positive integers and nothing else.8 of the 123 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 — 81 .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 — 12 .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 — 81 .php files:extensions/review_requests/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 — 81 .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 — 8 of 26 routes set a Content-Type of their own: 1 × 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) 3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 2 × none 1 × none set, and no output written 1 × none — a redirect back to the page, or the page re-rendered with the card in its error state 7 × none — a redirect back to the screen, with the outcome in the session 2 × none — nothing sets a Content-Type, so the store's default stands 2 × none — the 404 body is plain text with no type set 1 × none — the page goes out as core renders one 1 × none — the page goes out as core renders one, or a redirect back to the GET 1 × none — the screen goes out as core renders a page |
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 — 6 call sites in 3 templates, each declared with what it writes there: 3 × .append(3 × .prepend( |
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 — 81 .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 — 81 .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 — 10 routes of 26 reaches a model write; the 8 admin ones among them stand behind the user_token core checks before dispatch, and 17 admin routes are gated that way in all:extensions/review_requests/src/catalog/controller/review_requests/review_requests.php:65 — extension/review_requests/review_requests/review_requests 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.extensions/review_requests/src/catalog/controller/review_requests/review_requests.php:76 — extension/review_requests/review_requests/review_requests.submit 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 — 81 .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 — 81 .php files:extensions/review_requests/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 doesextensions/review_requests/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 doesextensions/review_requests/src/admin/controller/module/review_requests.php:1100 — ReviewRequests::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/admin/controller/module/review_requests.php:1462 — ReviewRequests::dismiss() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/admin/controller/module/review_requests.php:1586 — ReviewRequests::suppress() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/admin/controller/module/review_requests.php:1615 — ReviewRequests::unsuppress() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/admin/controller/module/review_requests.php:1719 — ReviewRequests::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/admin/controller/module/review_requests.php:2067 — ReviewRequests::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/catalog/controller/review_requests/review_requests.php:65 — ReviewRequests::index() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/review_requests/src/catalog/controller/review_requests/review_requests.php:76 — ReviewRequests::submit() 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 — 8 of 26 routes set a Content-Type of their own: 1 × 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) 3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 2 × none 1 × none set, and no output written 1 × none — a redirect back to the page, or the page re-rendered with the card in its error state 7 × none — a redirect back to the screen, with the outcome in the session 2 × none — nothing sets a Content-Type, so the store's default stands 2 × none — the 404 body is plain text with no type set 1 × none — the page goes out as core renders one 1 × none — the page goes out as core renders one, or a redirect back to the GET 1 × none — the screen goes out as core renders a page |
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 26 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 26 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 — 8 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:437system/library/diary.php:470 — the same kyvero.log, opened r+ to trim it back under the 1 MiB cap, at system/library/diary.php:470system/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 — 8 write sites, each declared with its file:line and pinned against the token stream both ways:3 × log file 5 × stream |
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 — 12 .twig files |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | checked — 12 .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 — 81 .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 — 26 routes: 17 admin, 9 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 — 17 admin routes: 15 pin a permission themselves, 0 at one same-class hop, 0 at two (the hop ceiling), 2 unpinned:extensions/review_requests/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/review_requests/customer/purge before dispatch. No model write is reachable from it.extensions/review_requests/src/admin/controller/module/review_requests.php:272 — ReviewRequests::index() pins no permission of its own; core checks access on extension/review_requests/module/review_requests 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 — 11 triples over 8 of 9 catalog routes; the admin half is one line on the shared page:extension/review_requests/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 nothingextension/review_requests/api/v1/ask — a holder of one of core's oc_api credentials: The credential cannot be scoped: there is no way to authorise a caller for one store, one order or one customer, which is why the contract says so in a disclosure rather than leaving a reader to find out. The contact address is returned in full for the same reason — redacting it would remove no access from anybody. Selected by ask_idextension/review_requests/api/v1/ask — a holder of one of core's oc_api credentials, walking the collection: filter_store_id is a convenience for a caller who wants one store, not a boundary: the same credential may simply leave it off. Selected by none — not a record; filter_store_id, filter_date_modified_from and the cursor are a page of a walk rather than a record selectorextension/review_requests/api/v1/run — a holder of one of core's oc_api credentials: Nothing here writes, and starting a pass is deliberately not a transition, so the resource is a read of what the scheduler already did. Selected by run_idextension/review_requests/cli/review_requests — whoever can run PHP on the server: Reached through extension/review_requests/review_requests.php, and refused outright to anything that is not a terminal. Selected by none — not a record; the arguments come from argv, which is not a request keyextension/review_requests/cron/review_requests — OpenCart's own scheduler, or a terminal on the store: There is no secret for this door: the proof is the framework passing an argument, or the absence of a request altogether, so a caller over HTTP arrives with zero under a web SAPI and is refused before a single message is spent. Selected by none — not a record; the route reads no request key at all and passes over every enabled storeextension/review_requests/review_requests/review_requests — the customer an emailed link was addressed to: One oc_token(40) string opens one order's review forms. It is authority rather than an identifier, which is why the API never emits it. Selected by tokenextension/review_requests/review_requests/review_requests — the customer an emailed link was addressed to, on a multi-store install: The token window and the wording both come from the store the link was sent for, so a link opened through another store front still reads its own store. Selected by none — not a record; the store is the one the token names, never the hostname the visitor arrived onextension/review_requests/review_requests/review_requests.submit — the customer an emailed link was addressed to: A line belonging to another order is refused by the same conditional UPDATE that guards the double submit — zero affected rows means already done or not yours, and the two are one answer. Selected by line_idextension/review_requests/review_requests/review_requests.submit — the customer an emailed link was addressed to: The token is read from the POST body first and the query string second; there is no session to ride, so a stashed copy would only be a second place for it to be wrong. Selected by tokenextension/review_requests/review_requests/review_requests.unsubscribe — the customer an emailed link was addressed to: The list is keyed on (store_id, email), so filing the opt-out against the hostname the visitor arrived on would silence them on a store that was never going to mail them. There is no date bound: refusing an opt-out for being old is the failure with teeth. Selected by token |
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 — 11 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 — the same unscoped credential; a run is a record of a pass over a store, and every store's passes are readable 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 the $cron_id argument core's dispatcher supplies — or the cli SAPI, which no request can present either; nothing in the request scopes anythingbounded by the (store_id, email) pair on the ask row the token names, never the ambient store bounded by the ask id in the claim statement's own WHERE clause bounded by the ask row the token names bounded by the ask row the token names, looked up by the token itself; the lines shown are that ask's own bounded by the ask row's own store_id bounded by the operating-system account the file is run as |
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 2 bearer-secret surfaces:catalog/model/review_requests/ask.php:64 — yes — one string opens an order's review forms and, on the same page, unsubscribes its customer, with no second factor; it is authority rather than an identifier, which is why no API field carries it |
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 — 81 .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 — 2 bearer-secret surfaces, from core, minted — never from anything inside the secret presented:catalog/model/review_requests/ask.php:64 — minted — oc_token(40) at catalog/model/review_requests/pass.php:1257; oc_token() is core's helper, but the mint is this extension'ssystem/library/api_gateway.php:948 — core |
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 2 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:catalog/model/review_requests/ask.php:64 — yes — one string opens an order's review forms and, on the same page, unsubscribes its customer, with no second factor; it is authority rather than an identifier, which is why no API field carries it |
11.3.1 |
Nothing encrypts with a broken mode or padding — no ECB, no PKCS#1 v1.5. | checked — 81 .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 — 81 .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 — 0 hash uses over 0 calls to 0 hash functions: This extension computes no hash. |
12.1.1 |
No outbound request asks for a TLS version below 1.2, and none pins itself to one at all. | checked — 81 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | checked — 81 .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 — 81 .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 — 1 bearer-secret surface of 2 travels in a URL:catalog/model/review_requests/ask.php:64 — whatever scheme the store's catalog is served on. The token travels in the query string of an emailed link and then in the page's own form, so it is in the mailbox, in the browser history and in any Referer the landing page emits |
14.3.1 |
Nothing is left behind in the browser's own storage for the next person at that computer to read. | checked — 12 .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 — 81 .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 — 47 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:action — request — the unsubscribe form's own action, with the bearer token in the query stringaddress — settings — config_name and config_address for the store the ask belongs toall_done — database — whether every line of the order has been reviewedapi_panel — settings and database — whether the API is on, and core's own oc_api rowsask — settings — the merchant's own wording blob for this store, with {store}, {name} and {date} filled inbackfill — settings — the configured backfill windowbackfill_modal — a rendered sub-template of this extension's own, carrying the subtrees abovebehind — database — how many orders are waiting to be askedcards — database — the order's lines: product names, images, and any review already written against themchecklist — settings and database — the readiness lines, each with what is wrong with itcode — database — the language code the readout is being written againstcopy_panel — settings and database — the merchant's own wording, per store and per languagefilter_email — request — what the merchant typed into the requests searchgreeting — settings — the merchant's own wording blob, with {store}, {name} and {date} filled inintro — settings — the merchant's own landing-page wordinglanguage — settings — the language code the page or the email is being rendered inlanguage_readout — database — the store's installed languages against what this extension shipslink — database — the landing-page address for this ask, with the bearer token in the query stringlogo — settings — config_logo for the store the ask belongs to, linked at its original sizemoderation — settings — the merchant's own wording about approvalmore — database — the product names of the lines beyond the three cardednotice — session — what the last post-redirect-get act left to saynotice_error — session — whether that notice is a failureorder_statuses — database — core's own order statusespasses — database — the recent pass records, with the error text of any that brokepills — database — a count per request stateready — settings and database — whether every checklist line passesreason — settings — the shipped reason sentence with the store name and the customer first name filled inrequest_filter — request — the order id, address and product the merchant filtered onrequest_filtered — request — whether any of those filters is setrequest_stores — database — the store names the requests list is grouped byrequests — database — the ask rows: customer addresses, order ids, states, and the transport error of any that failedstate — database — whether this address is on the suppression liststore — settings — config_name for the store the ask belongs tostore_id — request and database — which store the screen is working instore_url — settings — the base address of the store the ask belongs tostores — database and settings — the store names, config_name standing in for store 0suppressions — database — the suppression rows, which are customer addressessync_notice — settings and runtime — whether the note is due on this OpenCart version and whether the merchant put it awaytab — request — which tab of the screen is opentext_ask — language and database — the shipped sentence with the store name and the customer's own address in ittext_stopped — language and database — the shipped sentence with the store name and the customer's own address in ittext_unsubscribe — settings — the merchant's own wording for the footer linktitle — settings — the merchant's own subject line, with {store}, {name} and {date} filled intoken — request — the bearer token as it arrived, re-emitted into the form and the linksunsubscribe — database — the unsubscribe address for this ask, with the bearer token in the query stringuser_token — session — core's own admin session token |
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 — 81 .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 — 81 .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 — 81 .php files |