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 — 5 bearer-secret surfaces over 9 mint, compare and refuse sites (5 × mint, 4 × constant-time compare, 0 × compare, 0 × refusal); 3 further entries say what the derivation reached that is not a secret:
the API credential, presented as Authorization: Basic <base64 username:key> and compared at system/library/api_gateway.php:948; parsed at :904 and looked up against core's oc_api table by catalog/controller/event/api.php:164. 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 guest form handle, echoed back as form_handle in the submission (catalog/controller/account/returns_portal.php:855) and in the upload (:1078), compared both times with hash_equals(). It is a copy of the session grant below, so its entropy is that grant's: oc_token(32) at :581 and :1287. Compared in constant time at catalog/controller/account/returns_portal.php:855, catalog/controller/account/returns_portal.php:1078. No refusal of an unset value sits in this extension's own code. Minted with 128 bits of CSPRNG output at catalog/controller/account/returns_portal.php:581, catalog/controller/account/returns_portal.php:1287, which meets the 128-bit floor.
the session grant itself, minted as oc_token(32) at catalog/controller/account/returns_portal.php:581 and again in handle() at :1287. 32 hex characters is 128 bits. It is never presented by the caller — it lives in the session — but the handle above is a copy of it, which is what puts it here. 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 128 bits of CSPRNG output at catalog/controller/account/returns_portal.php:581, catalog/controller/account/returns_portal.php:1287, which meets the 128-bit floor.
core's customer_token, carried as a link fragment by token() (catalog/controller/account/returns_portal.php:685). Minted and rotated by core; this extension passes it along so that a link it builds keeps the session core gave it, and every account holder's write (submit, upload, deletePhoto, cancel) demands it back through holdsToken(), compared with hash_equals() at system/library/gate.php:338 and refused there when the session holds none. Core's account/login.validate is not used: 4.0.2.0 to 4.1.0.0 lack it, and asking for it there passed every request. Compared in constant time at system/library/gate.php:338. 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.
core's user_token, carried as a link fragment by every admin screen this extension builds — admin/controller/module/returns_portal.php:223 is where the fragment is assembled. Minted and checked by core's own admin startup. 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 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: 42 store-derived subtrees over 26 templates; unverified beyond it: everything else:
currency — store: the order's currency symbols from Currency::getSymbolLeft/Right() and the thousand and decimal points from the language file (catalog/controller/account/returns_portal.php:269). Rendered by catalog/view/template/account/returns_portal_order.twig.
currency — store: the same four values built the same way on the admin side (admin/controller/sale/request.php:491). The admin template interpolates the four strings into JavaScript string literals at admin/view/template/sale/request_info.twig:482-484, each through escape('js'); the catalog template crosses the same subtree as {{ currency|json_encode|escape('html_attr') }} (catalog/view/template/account/returns_portal_order.twig:132). Rendered by admin/view/template/sale/request_info.twig.
orders — store: the customer's own orders — order ids, dates, status names and totals out of getOrders(). Rendered by catalog/view/template/account/returns_portal_list.twig.
products — store: order lines — product names, models, option names and values, and money formatted through core. Rendered by catalog/view/template/account/returns_portal_order.twig, catalog/view/template/account/returns_portal_confirmation.twig, catalog/view/template/account/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_alert_submitted.twig, catalog/view/template/mail/returns_portal_rejected.twig, catalog/view/template/mail/returns_portal_slip.twig, admin/view/template/mail/returns_portal_alert_submitted.twig, admin/view/template/mail/returns_portal_rejected.twig, admin/view/template/mail/returns_portal_slip.twig.
breakdown — store: the four refund components, each formatted through core at the rate the order was placed at. Rendered by catalog/view/template/account/returns_portal_order.twig, catalog/view/template/account/returns_portal_confirmation.twig, catalog/view/template/account/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_alert_submitted.twig, catalog/view/template/mail/returns_portal_slip.twig, admin/view/template/sale/request_info.twig, admin/view/template/mail/returns_portal_alert_submitted.twig, admin/view/template/mail/returns_portal_slip.twig.
amount — store: one money figure, formatted through core. Rendered by catalog/view/template/account/returns_portal_order.twig, catalog/view/template/mail/returns_portal_credit.twig, catalog/view/template/mail/returns_portal_alert_credit_failed.twig, admin/view/template/sale/request_info.twig, admin/view/template/mail/returns_portal_credit.twig, admin/view/template/mail/returns_portal_alert_credit_failed.twig.
request — store: one request row — the customer's name and email, their typed comment, the resolution and the state. Rendered by admin/view/template/sale/request_info.twig, admin/view/template/sale/request.twig.
requests — store: request rows for one email address, on the erasure screen. Rendered by admin/view/template/customer/erasure.twig.
results — store: one page of rows — request rows on the admin lists and on the erasure screen, order rows on the catalog list. Rendered by admin/view/template/sale/request_list.twig, admin/view/template/sale/request_unmanaged.twig, admin/view/template/customer/erasure.twig, catalog/view/template/account/returns_portal_list.twig.
lines — store: the lines of one request — product names, the customer's reason and comment, the verdicts. Rendered by admin/view/template/sale/request_info.twig.
histories — store: the history rows of one request, each carrying a comment an administrator typed and a customer may read. Rendered by admin/view/template/sale/request_info.twig.
returns — store: core's own oc_return rows mirrored against the request. Rendered by admin/view/template/sale/request_info.twig, admin/view/template/sale/request_unmanaged.twig.
credit — store: the ledger row for one request — the amount issued and its currency code. Rendered by admin/view/template/sale/request_info.twig.
refunds — store: the refunds recorded against one request — amounts formatted through core, the request's currency code, the username of whoever recorded each, and method, prose a member of staff typed and core's request cleaning escaped on the way in. Rendered by admin/view/template/sale/request_info.twig.
customer — store: the customer's first and last name, off the request row. Rendered by catalog/view/template/mail/returns_portal_cancelled.twig, catalog/view/template/mail/returns_portal_credit.twig, catalog/view/template/mail/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_alert_submitted.twig, catalog/view/template/mail/returns_portal_alert_credit_failed.twig, admin/view/template/sale/request_info.twig, admin/view/template/customer/erasure.twig, admin/view/template/mail/returns_portal_slip.twig.
email — store: the email address on the request row, or the one typed into the erasure form. Rendered by catalog/view/template/account/returns_portal_confirmation.twig, catalog/view/template/account/returns_portal_lookup.twig, catalog/view/template/mail/returns_portal_alert_submitted.twig, admin/view/template/sale/request_info.twig, admin/view/template/customer/erasure.twig.
greeting — operator: the merchant's own wording for the greeting, out of the Copy setting, with {name} substituted — falling back to our shipped sentence where they have written none. Rendered by catalog/view/template/mail/returns_portal_cancelled.twig, catalog/view/template/mail/returns_portal_credit.twig, catalog/view/template/mail/returns_portal_rejected.twig, catalog/view/template/mail/returns_portal_slip.twig, admin/view/template/mail/returns_portal_slip.twig.
return_address — operator: the address the merchant typed into the settings screen. Rendered by catalog/view/template/account/returns_portal_confirmation.twig, catalog/view/template/account/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_slip.twig, admin/view/template/mail/returns_portal_slip.twig.
return_instructions — operator: the instructions the merchant typed into the settings screen. Rendered by catalog/view/template/account/returns_portal_confirmation.twig, catalog/view/template/account/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_slip.twig, admin/view/template/mail/returns_portal_slip.twig.
reason — store: on the mail side the comment attached to the transition; on the order screen the localisation's own return-reason names. Rendered by catalog/view/template/account/returns_portal_order.twig, catalog/view/template/mail/returns_portal_cancelled.twig, catalog/view/template/mail/returns_portal_rejected.twig, admin/view/template/module/returns_portal.twig, admin/view/template/mail/returns_portal_cancelled.twig, admin/view/template/mail/returns_portal_rejected.twig.
return_reasons — store: oc_return_reason names for the current language. Rendered by catalog/view/template/account/returns_portal_order.twig.
refusal — store: the sentence naming why an upload was refused, carrying the actual byte count of the file the customer chose. Rendered by catalog/view/template/account/returns_portal_order.twig, admin/view/template/sale/request_info.twig.
store_name — store: the order's own store name, or config_name, with html_entity_decode() applied because a mail body is not a Twig page. Rendered by catalog/view/template/mail/returns_portal_cancelled.twig, catalog/view/template/mail/returns_portal_credit.twig, catalog/view/template/mail/returns_portal_rejected.twig, catalog/view/template/mail/returns_portal_slip.twig, catalog/view/template/mail/returns_portal_alert_submitted.twig, catalog/view/template/mail/returns_portal_alert_credit_failed.twig, admin/view/template/mail/returns_portal_slip.twig.
store_url — store: the order's own store URL, used to build the account link in a mail. Rendered by admin/view/template/module/returns_portal.twig.
stores — store: the names of the stores on a multi-store install. Rendered by admin/view/template/module/returns_portal.twig, admin/view/template/module/api_panel.twig, admin/view/template/sale/request.twig.
order_statuses — store: oc_order_status names for the current language. Rendered by admin/view/template/module/returns_portal.twig.
excluded_products — store: product names resolved from the ids the merchant excluded. Rendered by admin/view/template/module/returns_portal.twig.
excluded_categories — store: category paths resolved from the ids the merchant excluded. Rendered by admin/view/template/module/returns_portal.twig.
captchas — store: the codes of the captcha extensions the store has installed. Rendered by admin/view/template/module/returns_portal.twig.
captcha — store: the captcha extension's own rendered output, which is another extension's markup. Rendered by catalog/view/template/account/returns_portal_lookup.twig, admin/view/template/module/returns_portal.twig.
filter_customer — request: the admin's own search term, echoed back into the form. Rendered by admin/view/template/sale/request.twig.
filter_order_id — request: the admin's own search term, echoed back into the form. Rendered by admin/view/template/sale/request.twig.
filter_rma — request: the admin's own search term, echoed back into the form. Rendered by admin/view/template/sale/request.twig.
filter_email — request: the email typed into the erasure screen, echoed back into the form and — at admin/view/template/customer/erasure.twig:152 — into a JavaScript string literal, there with escape('js'). Rendered by admin/view/template/customer/erasure.twig.
queue — store: the erasure queue — request rows already marked for erasure, with the customer data still on them. Rendered by admin/view/template/customer/erasure.twig.
plan — store: the counts the erasure screen prints for one email address. Rendered by admin/view/template/customer/erasure.twig.
subject — store: what is known about the person behind one email address. Rendered by admin/view/template/customer/erasure.twig.
api_panel — store: a rendered sub-view carrying the store's API credentials' usernames and the store names beside them. Rendered by admin/view/template/module/returns_portal.twig, admin/view/template/module/api_panel.twig.
copy_panel — operator: a rendered sub-view carrying the merchant's own wording, per language. Rendered by admin/view/template/module/returns_portal.twig, admin/view/template/module/copy_panel.twig.
language_readout — store: a rendered sub-view carrying the names of the languages the store has installed. Rendered by admin/view/template/module/returns_portal.twig, admin/view/template/module/language_readout.twig.
form_handle — minted: the oc_token(32) form handle, rendered into the order form so the submission can echo it back. Rendered by catalog/view/template/account/returns_portal_order.twig.
version_warning — store: the sentence naming the store's own OpenCart version where it is outside the tested range. Rendered by admin/view/template/module/returns_portal.twig.
Unverified beyond it: the other 1334 of 1389 template expressions in 30 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 — 55 url attributes carrying a template expression, of 55 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 — 30 .twig files:
extensions/returns_portal/src/admin/view/template/customer/erasure.twig:154 — {{ erase }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/module/returns_portal.twig:348 — {{ user_token }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/module/returns_portal.twig:381 — {{ user_token }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/module/returns_portal.twig:415 — {{ grant }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request.twig:78 — {{ bucket }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request.twig:104 — {{ user_token }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request.twig:106 — {{ user_token }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:387 — {{ mirror }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:416 — {{ text_save_refused }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:416 — {{ text_accept_all }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:423 — {{ text_save_refusal }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:433 — {{ decide }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:437 — {{ decide }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:445 — {{ close }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:449 — {{ cancel }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:458 — {{ text_total }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:471 — {{ text_shown }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:472 — {{ text_accepting }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:480 — {{ currency.value }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:480 — {{ currency.decimal_place }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:532 — {{ text_photo_confirm }} is interpolated inside a <script> element
extensions/returns_portal/src/admin/view/template/sale/request_info.twig:647 — {{ text_refund_confirm }} is interpolated inside a <script> element
extensions/returns_portal/src/catalog/view/template/account/returns_portal_confirmation.twig:136 — {{ text_cancel_confirm }} is interpolated inside a <script> element
extensions/returns_portal/src/catalog/view/template/account/returns_portal_confirmation.twig:141 — {{ cancel }} 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 160 statements run and 92 values escaped:
escaped string literal — '" . $this->db->escape($value) . "' inside a quoted literal — core's own idiom, and the mechanism every free-text value goes through: email addresses, comments, filter terms and API usernames
integer cast into a literal — '" . (int)$id . "' — every identifier, every status and every limit. A cast leaves no way for the value to carry a quote, which is why it is a mechanism of its own and not a variant of the one above
interpolated identifier — DB_PREFIX . $table in the four schema-introspection statements at admin/model/module/returns_portal.php:226, :234, :259 and :267 (SHOW TABLES LIKE, SHOW COLUMNS FROM, SHOW INDEX FROM). The names come from Schema's own constants — this extension's table list — and never from a request
whole-clause concatenation — one site: admin/model/customer/erasure.php:108-116 builds $where from an escaped email literal and, where there are matching customers, an IN (…) of integers joined with implode(), then interpolates the whole clause into the statement. The integers come from getCustomerIds() and are cast before they reach the join
12 of the 160 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 — 82 .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 — 30 .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 — 82 .php files:
extensions/returns_portal/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 — 82 .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 — 30 of 47 routes set a Content-Type of their own:
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/attachment.php:265. Reads the raw $_GET (:55)
2 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638. Reads the raw $_GET (:83) and the raw $_SERVER (:183, :638)
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638. The body is read with ApiReader::body(..., $_SERVER, php://input) at :183, off the raw superglobal
1 × application/json, with no charset (account/returns_portal.php:1026)
1 × application/json, with no charset (account/returns_portal.php:1137)
1 × application/json, with no charset (account/returns_portal.php:1183)
1 × application/json, with no charset (account/returns_portal.php:1473)
1 × application/json, with no charset (account/returns_portal.php:496)
1 × application/json, with no charset (customer/erasure.php:186)
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 (module/returns_portal.php:621)
1 × application/json, with no charset (module/returns_portal.php:664)
1 × application/json, with no charset (sale/request.php:1097)
1 × application/json, with no charset (sale/request.php:1140)
1 × application/json, with no charset (sale/request.php:1337)
1 × application/json, with no charset (sale/request.php:588)
2 × application/json, with no charset (sale/request.php:674)
1 × application/json, with no charset (sale/request.php:718)
1 × application/json, with no charset (sale/request.php:779)
1 × computed: ApiAnswer::download($_SERVER, ...) builds the list from the attachment's stored mime column (api/v1/attachment.php:133), the type Photos::mime() verified at upload. No Content-Length, because the answer inherits the merchant's compression setting. Reads the raw $_GET (:93) and the raw $_SERVER (:133)
1 × computed: Content-Type: . the attachment's stored mime column (account/returns_portal.php:1230), the type Photos::mime() verified at upload and never the client's own header. No charset, and X-Content-Type-Options: nosniff beside it at :1233
1 × computed: Content-Type: . the attachment's stored mime column (sale/request.php:1261), which is the type Photos::mime() verified at upload and never the client's own header. No charset, and X-Content-Type-Options: nosniff beside it at :1264
1 × none set, and no output written
1 × none — it lowers the spelling of one of two routes and writes no response
15 × none — nothing sets a Content-Type, so the store's default stands
1 × set by ApiAnswer::headers($_SERVER, ...), whose list carries Content-Type: application/json; charset=utf-8. This route reads the raw $_SERVER (api/gateway.php:45) rather than $this->request->server
1 × text/csv; charset=utf-8 — beside a Content-Disposition naming returns--.csv, where the bucket is one of the queue's own keys and never merchant text, and 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 — 44 call sites in 7 templates, each declared with what it writes there:
12 × .append(
13 × .html(
19 × .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 — 82 .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 — 82 .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 — 13 routes of 47 reaches a model write; the 9 admin ones among them stand behind the user_token core checks before dispatch, and 28 admin routes are gated that way in all:
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:433 — extension/returns_portal/account/returns_portal.lookup 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/returns_portal/src/catalog/controller/account/returns_portal.php:830 — extension/returns_portal/account/returns_portal.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.
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:1050 — extension/returns_portal/account/returns_portal.upload 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/returns_portal/src/catalog/controller/account/returns_portal.php:1157 — extension/returns_portal/account/returns_portal.deletePhoto 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 — 82 .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 — 82 .php files:
extensions/returns_portal/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/returns_portal/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/returns_portal/src/admin/controller/module/returns_portal.php:494 — ReturnsPortal::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/module/returns_portal.php:670 — ReturnsPortal::grant() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/module/returns_portal.php:698 — ReturnsPortal::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/module/returns_portal.php:838 — ReturnsPortal::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/sale/request.php:1052 — Request::refund() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/sale/request.php:1109 — Request::deleteRefund() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/admin/controller/sale/request.php:1291 — Request::deletePhoto() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:433 — ReturnsPortal::lookup() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:830 — ReturnsPortal::submit() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:1050 — ReturnsPortal::upload() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST does
extensions/returns_portal/src/catalog/controller/account/returns_portal.php:1157 — ReturnsPortal::deletePhoto() 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 — 30 of 47 routes set a Content-Type of their own:
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/attachment.php:265. Reads the raw $_GET (:55)
2 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638. Reads the raw $_GET (:83) and the raw $_SERVER (:183, :638)
1 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...) at api/v1/request.php:638. The body is read with ApiReader::body(..., $_SERVER, php://input) at :183, off the raw superglobal
1 × application/json, with no charset (account/returns_portal.php:1026)
1 × application/json, with no charset (account/returns_portal.php:1137)
1 × application/json, with no charset (account/returns_portal.php:1183)
1 × application/json, with no charset (account/returns_portal.php:1473)
1 × application/json, with no charset (account/returns_portal.php:496)
1 × application/json, with no charset (customer/erasure.php:186)
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 (module/returns_portal.php:621)
1 × application/json, with no charset (module/returns_portal.php:664)
1 × application/json, with no charset (sale/request.php:1097)
1 × application/json, with no charset (sale/request.php:1140)
1 × application/json, with no charset (sale/request.php:1337)
1 × application/json, with no charset (sale/request.php:588)
2 × application/json, with no charset (sale/request.php:674)
1 × application/json, with no charset (sale/request.php:718)
1 × application/json, with no charset (sale/request.php:779)
1 × computed: ApiAnswer::download($_SERVER, ...) builds the list from the attachment's stored mime column (api/v1/attachment.php:133), the type Photos::mime() verified at upload. No Content-Length, because the answer inherits the merchant's compression setting. Reads the raw $_GET (:93) and the raw $_SERVER (:133)
1 × computed: Content-Type: . the attachment's stored mime column (account/returns_portal.php:1230), the type Photos::mime() verified at upload and never the client's own header. No charset, and X-Content-Type-Options: nosniff beside it at :1233
1 × computed: Content-Type: . the attachment's stored mime column (sale/request.php:1261), which is the type Photos::mime() verified at upload and never the client's own header. No charset, and X-Content-Type-Options: nosniff beside it at :1264
1 × none set, and no output written
1 × none — it lowers the spelling of one of two routes and writes no response
15 × none — nothing sets a Content-Type, so the store's default stands
1 × set by ApiAnswer::headers($_SERVER, ...), whose list carries Content-Type: application/json; charset=utf-8. This route reads the raw $_SERVER (api/gateway.php:45) rather than $this->request->server
1 × text/csv; charset=utf-8 — beside a Content-Disposition naming returns--.csv, where the bucket is one of the queue's own keys and never merchant text, and 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 — 1 upload surface across 47 routes:
extension/returns_portal/account/returns_portal.upload — $this->request->files['file'] (account/returns_portal.php:1084), the extension's only upload surface. Photos::verify() refuses unless the browser's own error is UPLOAD_ERR_OK and is_uploaded_file() holds — core's own uploaders test is_file(), which admits any path the request can name. The type is decided by Photos::mime() (system/library/photos.php:288), which takes getimagesize()'s type and finfo's magic-byte sniff and accepts only where the two agree; a store with no ext-fileinfo degrades to getimagesize() alone, and a disagreement is refused as a polyglot. The client-supplied MIME type is never read, anywhere in this extension. The stored name is oc_token(32) plus the extension of the verified type, so nothing the caller named survives into the filesystem; the caller's own name is kept as a label only. Also bounded: the suffix against a list, the byte count against Photos::MAX_BYTES, and the count per line and per request
5.2.2 An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands. declared — 1 upload surface across 47 routes:
extension/returns_portal/account/returns_portal.upload — $this->request->files['file'] (account/returns_portal.php:1084), the extension's only upload surface. Photos::verify() refuses unless the browser's own error is UPLOAD_ERR_OK and is_uploaded_file() holds — core's own uploaders test is_file(), which admits any path the request can name. The type is decided by Photos::mime() (system/library/photos.php:288), which takes getimagesize()'s type and finfo's magic-byte sniff and accepts only where the two agree; a store with no ext-fileinfo degrades to getimagesize() alone, and a disagreement is refused as a polyglot. The client-supplied MIME type is never read, anywhere in this extension. The stored name is oc_token(32) plus the extension of the verified type, so nothing the caller named survives into the filesystem; the caller's own name is kept as a label only. Also bounded: the suffix against a list, the byte count against Photos::MAX_BYTES, and the count per line and per request
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 — 12 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 — 12 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
1 × fixed column names
3 × log file
1 × mkdir
1 × move_uploaded_file
1 × the queue's rows: request, order, contact, product, verdict, estimate and restock
4 × unlink
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 — 30 .twig files
6.2.7 A masked field does not refuse a paste or shut a password manager out of it. checked — 30 .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 — 82 .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 — 47 routes: 28 admin, 19 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 — 28 admin routes: 16 pin a permission themselves, 2 at one same-class hop, 0 at two (the hop ceiling), 10 unpinned:
extensions/returns_portal/src/admin/controller/customer/erasure.php:63 — Erasure::index() pins no permission of its own; core checks access on extension/returns_portal/customer/erasure before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/returns_portal/customer/purge before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/module/returns_portal.php:229 — ReturnsPortal::index() pins no permission of its own; core checks access on extension/returns_portal/module/returns_portal before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:59 — Request::index() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:147 — Request::list() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:158 — Request::getList() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:260 — Request::export() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:299 — Request::info() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:1244 — Request::photo() pins no permission of its own; core checks access on extension/returns_portal/sale/request before dispatch. No model write is reachable from it.
extensions/returns_portal/src/admin/controller/sale/request.php:1356 — Request::unmanaged() pins no permission of its own; core checks access on extension/returns_portal/sale/request 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 — 26 triples over 18 of 19 catalog routes; the admin half is one line on the shared page:
extension/returns_portal/account/returns_portal — account holder: The list is getOrders(), which is filtered on the logged-in customer; page only moves the window over rows that were already theirs. Selected by page (get) — none — not a record
extension/returns_portal/account/returns_portal — guest holding no grant: Somebody with no account never reaches the list: index() branches to lookupForm() before any record is read. Selected by none — not a record
extension/returns_portal/account/returns_portal.order — account holder: An account holder reads the order through the customer-scoped query, so an id belonging to somebody else returns nothing and the route answers error/not_found. Selected by order_id (get)
extension/returns_portal/account/returns_portal.order — guest holding a grant: The session grant names one order. Gate::granted() is called with order_id 0 (account/returns_portal.php:602), so the order binding is not tested there; it is tested here, in the controller, and a mismatch redirects to the lookup rather than to an error page. Selected by order_id (get)
extension/returns_portal/account/returns_portal.lookup — anybody at all: This is the route that mints authority rather than one that checks it: the order id alone proves nothing, and the grant it issues records the order, the moment and who the session belonged to. Selected by order_id (post) with email (post)
extension/returns_portal/account/returns_portal.submit — account holder: The lines are checked against the order that was read, not against the post. Selected by order_id (post), line (post)
extension/returns_portal/account/returns_portal.submit — guest holding a grant: Gate::granted() never sees the order id on this path either; the binding is the inline compare, and the handle the form echoes back has to be the grant's own. Selected by order_id (post), form_handle (post)
extension/returns_portal/account/returns_portal.upload — account holder: Both the order and the line have to come back from the database before a byte is read. Selected by order_id (post), order_product_id (post)
extension/returns_portal/account/returns_portal.upload — guest holding a grant: This is the third of the three inline comparisons; a rule concluding that the order binding lives in Gate::granted() would be wrong at all three, because the one call to it passes 0 for the order. Selected by order_id (post), order_product_id (post), form_handle (post)
extension/returns_portal/account/returns_portal.deletePhoto — account holder or guest holding a grant: Pending rows only: after submission the photograph is the merchant's evidence, and the merchant's delete is a separate act on a separate screen. Selected by returns_portal_attachment_id (post), order_id (post)
extension/returns_portal/account/returns_portal.photo — account holder: Tried only after the handles below have missed. Selected by returns_portal_attachment_id (get)
extension/returns_portal/account/returns_portal.photo — guest holding a grant: The id on its own reaches nothing: it is resolved by (form handle, id), so an id leaked out of one session is inert in another. Selected by returns_portal_attachment_id (get)
extension/returns_portal/account/returns_portal.cancel — account holder: The store id is tested too, so a request raised on one store of a multi-store install is not cancellable from another. Selected by return_request_id (get)
extension/returns_portal/account/returns_portal.cancel — guest: A guest may cancel only a request this session raised; a draft is readable by nobody at all, including whoever started it. Selected by return_request_id (get)
extension/returns_portal/account/returns_portal.confirmation — account holder: Read through render(), which resolves the id before it builds anything. Selected by return_request_id (get)
extension/returns_portal/account/returns_portal.confirmation — guest: Same resolution as the cancel route, because it is the same helper. Selected by return_request_id (get)
extension/returns_portal/account/returns_portal.slip — account holder: The slip is a document of its own rather than a page of the store, and it is resolved exactly as the confirmation is. Selected by return_request_id (get)
extension/returns_portal/account/returns_portal.slip — guest: A guest printing a slip is printing one this session raised. Selected by return_request_id (get)
extension/returns_portal/startup/door — any visitor to the storefront: It changes the case of account/returns.add or .save and nothing else, so the door rows see the request the controller is about to answer. Selected by none — not a record; it reads only the route
extension/returns_portal/api/gateway.fail — any caller, credentialled or not: A refusal envelope reads nothing and partitions nothing; it exists so that a convention failure cannot answer with a different header list from a success. Selected by none — not a record
extension/returns_portal/api/v1/request — the merchant's own integrator, holding an oc_api credential: The API is a merchant-to-merchant surface: there is no second API identity to partition against, and the filters a caller may pass narrow the answer rather than bounding it. Selected by request_id (get), or a filtered page where it is absent
extension/returns_portal/api/v1/request.decide — the merchant's own integrator, holding an oc_api credential: A transition the current status does not allow is transition_not_allowed, which is a lifecycle refusal and not an authorisation one. Selected by request_id (get) — the only parameter a transition accepts
extension/returns_portal/api/v1/request.close — the merchant's own integrator, holding an oc_api credential: Closing may debit the merchant where their own automatic-credit setting fires on this arrow, and may change stock where their restock setting is on, which is the merchant acting on their own store. Selected by request_id (get), not_restocked (body)
extension/returns_portal/api/v1/request.cancel — the merchant's own integrator, holding an oc_api credential: The customer-facing cancel is a different route with a different identity; this one is the merchant cancelling on the customer's behalf. Selected by request_id (get)
extension/returns_portal/api/v1/attachment — the merchant's own integrator, holding an oc_api credential: The customer-facing photo route resolves by (form handle, id); this one does not, because the credential is the merchant's. Selected by attachment_id (get), or a filtered page where it is absent
extension/returns_portal/api/v1/attachment.content — the merchant's own integrator, holding an oc_api credential: The bytes come from outside the document root, so the only way to them is this route. Selected by attachment_id (get)
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 — 22 distinct bounds, each named by the triple it scopes:
bounded by authorised() plus owns(), which asks the order for its lines rather than trusting the post (account/returns_portal.php:1345); authorised() demands core's customer_token (validated(), :1860)
bounded by authorised() reading the order through the customer-scoped query (account/returns_portal.php:1791), behind core's customer_token (validated(), :1860)
bounded by getOwnAttachment(), which joins on the session customer id (account/returns_portal.php:1219)
bounded by viewable() matching the row's customer_id against the session customer (account/returns_portal.php:1773)
bounded by core's customer_token (validated(), account/returns_portal.php:1463), then viewable() matching the row's customer_id against the session customer (:1773), then cancel() repeating the customer id in the statement (:1465)
bounded by nothing — getAttachment() resolves the id across the whole store
bounded by nothing — getRequest() resolves the id across the whole store, because the credential is the merchant's and every request of the store is theirs to read
bounded by nothing — no record is read
bounded by nothing — store-wide, as above
bounded by nothing — store-wide, as above; not_restocked only ever narrows the accepted lines of that one request
bounded by nothing — store-wide, as above; the transition itself is bounded by the lifecycle rather than by identity
bounded by nothing; it addresses no record at all
bounded by nothing; the route renders the lookup form and reads no record at all
bounded by the form handle handle() returns for that order, and the attachment's own order_id compared at account/returns_portal.php:1171; an account holder also carries core's customer_token (validated(), :1173)
bounded by the form handles this session could legitimately hold (handles(), account/returns_portal.php:1316), each tried against the id
bounded by the grant's order_id compared inline at account/returns_portal.php:1802 and in handle() at :1274, and the handle compared with hash_equals() at :1078
bounded by the grant's order_id compared inline at account/returns_portal.php:1802, and the handle compared with hash_equals() at :855
bounded by the grant's own order_id, compared inline at account/returns_portal.php:205
bounded by the pair having to match a row; a grant is issued only where it does
bounded by the session customer id, inside getOrder()
bounded by the session customer id, inside the model
bounded by the session's own list of request ids (account/returns_portal.php:1767)
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 — 0 self-contained surfaces of 5 bearer-secret surfaces
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 — 82 .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 — 5 bearer-secret surfaces, from core, minted — never from anything inside the secret presented:
system/library/api_gateway.php:948 — core
catalog/controller/account/returns_portal.php:581 — minted
catalog/controller/account/returns_portal.php:581 — minted
system/library/gate.php:338 — core
admin/controller/module/returns_portal.php:223 — 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 — 0 surfaces of 5 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:
No secret this extension accepts carries its own validity span.
11.3.1 Nothing encrypts with a broken mode or padding — no ECB, no PKCS#1 v1.5. checked — 82 .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 — 82 .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 — 82 .php files
12.2.1 An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. checked — 82 .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 — 82 .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 5 travel in a URL:
system/library/gate.php:338 — the query string of every storefront link this extension builds
admin/controller/module/returns_portal.php:223 — the query string of every admin link this extension builds
14.3.1 Nothing is left behind in the browser's own storage for the next person at that computer to read. checked — 30 .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 — 82 .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 — 42 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:
currency — store: the order's currency symbols from Currency::getSymbolLeft/Right() and the thousand and decimal points from the language file (catalog/controller/account/returns_portal.php:269)
currency — store: the same four values built the same way on the admin side (admin/controller/sale/request.php:491). The admin template interpolates the four strings into JavaScript string literals at admin/view/template/sale/request_info.twig:482-484, each through escape('js'); the catalog template crosses the same subtree as {{ currency|json_encode|escape('html_attr') }} (catalog/view/template/account/returns_portal_order.twig:132)
orders — store: the customer's own orders — order ids, dates, status names and totals out of getOrders()
products — store: order lines — product names, models, option names and values, and money formatted through core
breakdown — store: the four refund components, each formatted through core at the rate the order was placed at
amount — store: one money figure, formatted through core
request — store: one request row — the customer's name and email, their typed comment, the resolution and the state
requests — store: request rows for one email address, on the erasure screen
results — store: one page of rows — request rows on the admin lists and on the erasure screen, order rows on the catalog list
lines — store: the lines of one request — product names, the customer's reason and comment, the verdicts
histories — store: the history rows of one request, each carrying a comment an administrator typed and a customer may read
returns — store: core's own oc_return rows mirrored against the request
credit — store: the ledger row for one request — the amount issued and its currency code
refunds — store: the refunds recorded against one request — amounts formatted through core, the request's currency code, the username of whoever recorded each, and method, prose a member of staff typed and core's request cleaning escaped on the way in
customer — store: the customer's first and last name, off the request row
email — store: the email address on the request row, or the one typed into the erasure form
greeting — operator: the merchant's own wording for the greeting, out of the Copy setting, with {name} substituted — falling back to our shipped sentence where they have written none
return_address — operator: the address the merchant typed into the settings screen
return_instructions — operator: the instructions the merchant typed into the settings screen
reason — store: on the mail side the comment attached to the transition; on the order screen the localisation's own return-reason names
return_reasons — store: oc_return_reason names for the current language
refusal — store: the sentence naming why an upload was refused, carrying the actual byte count of the file the customer chose
store_name — store: the order's own store name, or config_name, with html_entity_decode() applied because a mail body is not a Twig page
store_url — store: the order's own store URL, used to build the account link in a mail
stores — store: the names of the stores on a multi-store install
order_statuses — store: oc_order_status names for the current language
excluded_products — store: product names resolved from the ids the merchant excluded
excluded_categories — store: category paths resolved from the ids the merchant excluded
captchas — store: the codes of the captcha extensions the store has installed
captcha — store: the captcha extension's own rendered output, which is another extension's markup
filter_customer — request: the admin's own search term, echoed back into the form
filter_order_id — request: the admin's own search term, echoed back into the form
filter_rma — request: the admin's own search term, echoed back into the form
filter_email — request: the email typed into the erasure screen, echoed back into the form and — at admin/view/template/customer/erasure.twig:152 — into a JavaScript string literal, there with escape('js')
queue — store: the erasure queue — request rows already marked for erasure, with the customer data still on them
plan — store: the counts the erasure screen prints for one email address
subject — store: what is known about the person behind one email address
api_panel — store: a rendered sub-view carrying the store's API credentials' usernames and the store names beside them
copy_panel — operator: a rendered sub-view carrying the merchant's own wording, per language
language_readout — store: a rendered sub-view carrying the names of the languages the store has installed
form_handle — minted: the oc_token(32) form handle, rendered into the order form so the submission can echo it back
version_warning — store: the sentence naming the store's own OpenCart version where it is outside the tested range
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 — 82 .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 — 82 .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 — 82 .php files