Security verdict¶
21 checked, 2 not met, 22 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 — 1 bearer-secret surface over 3 mint, compare and refuse sites (2 × mint, 1 × constant-time compare, 0 × compare, 0 × refusal); 1 further entry says what the derivation reached that is not a secret: the HTTP Basic key on the two 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: 18 store-derived subtrees over 9 templates; unverified beyond it: everything else:module_gift_cards_status — the module_gift_cards setting. Rendered by admin/view/template/module/gift_cards.twig.module_gift_cards_validity_months — the module_gift_cards setting, cast to (int) by the controller before it reaches the template — so what the number input is rendered with is a number whatever the setting table came to hold. Rendered by admin/view/template/module/gift_cards.twig.expires_on — the gift_cards_card row for the voucher whose form this is, put through Expiry::date() and then htmlspecialchars() by the listener that splices the field in — admin Twig has autoescape off in both releases, which is why the escaping is at the call site and not in the partial. Rendered by admin/view/template/module/gift_cards_expiry.twig.legal_floors_warns — the same module_gift_cards setting, put through LegalFloors::warns() — so what the template renders is a boolean rather than the number, and it decides one Bootstrap class on an element whose text is a constant in this repository. The rows beside it (legal_floors, text_not_exhaustive, text_floor_source) come from LegalFloors and the language file and carry nothing the store holds. Rendered by admin/view/template/module/gift_cards.twig.module_gift_cards_lookup — the module_gift_cards setting. Rendered by admin/view/template/module/gift_cards.twig.report_gift_cards_status — the report_gift_cards setting. Rendered by admin/view/template/report/gift_cards_form.twig.report_gift_cards_sort_order — the report_gift_cards setting. Rendered by admin/view/template/report/gift_cards_form.twig.headline — the outstanding-liability figure, summed from oc_voucher and oc_voucher_history by Liability::figures() and already formatted by $this->currency->format() in the controller. Rendered by admin/view/template/report/gift_cards.twig.headline_count — how many cards that figure is over, an integer counted by Liability::figures(). Rendered by admin/view/template/report/gift_cards.twig.companions — the four figures beside the headline: each one's name, its placement sentence and the reason for it come from this extension's own language file, and its amount and count from Liability::figures() over the same two core tables, already formatted in the controller. Rendered by admin/view/template/report/gift_cards.twig.cards — one page of the oc_voucher rows any liability figure counts — the card code, the recipient's name, the purchasing order id, two amounts already formatted by $this->currency->format(), this extension's own expiry date, the five booleans the row's markers are decided from and the placement sentence out of this extension's own language file. The recipient's email address, the buyer's name and address, and the card's message are not selected by the report's model, so the subtree cannot carry them. Rendered by admin/view/template/report/gift_cards_list.twig.list — this extension's own rendered table of the cards behind the figure. Rendered by admin/view/template/report/gift_cards.twig.results — the paging sentence out of this extension's own language file, with four integers counted in the controller. Rendered by admin/view/template/report/gift_cards_list.twig.card — one oc_voucher row a visitor proved with its code and its to_email, plus its oc_voucher_history rows — every figure already formatted by $this->currency->format() in the controller, and every history line already reduced to a date and an amount by Balance::lines(). The recipient's name, the buyer's name, the buyer's address, the message and the order each redemption sat on are not selected, so the subtree cannot carry them. Rendered by catalog/view/template/account/balance.twig.code — the card code the visitor posted, echoed back into the form so a mistyped address is not a retyped code. Printed as it arrived: OpenCart runs Twig with autoescape off, and the value is safe because Request::clean() entity-encoded the post before this page read it; the address is deliberately not echoed. Rendered by catalog/view/template/account/balance.twig.balance_link — this extension's own route, built by $this->url->link() in the listener that splices the link into core's footer — so the only part of it the store decides is config_language, and Url::link() has already put it into the & form an HTML attribute wants. It is deliberately not escaped a second time, for the reason core prints its own footer links the same way. The label beside it is this extension's own heading_title, escaped at that call site because storefront Twig has autoescape off in both releases. Rendered by catalog/view/template/account/balance_link.twig.text_validity — the module_gift_cards validity period, cast to (int) and concatenated into a sentence out of this extension's own language file by the listener that splices it into core's gift card purchase form — then put through htmlspecialchars() there, because storefront Twig has autoescape off in both releases. The number is the only part of it the store decides, and it is an integer by the time it is anywhere near the string. Rendered by catalog/view/template/checkout/voucher_validity.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/gift_cards.twig.Unverified beyond it: the other 299 of 322 template expressions in 14 templates, and any $data subtree nobody enumerated. The inventory is an inventory and not a bound: completeness over the whole expression surface is unverifiable, so this residual is permanent, and it is published rather than left to be inferred from what is missing. |
1.2.2 |
A URL a template builds for itself, rather than taking one whole from the link helper, has every value in it URL-encoded — so nothing a store holds can add a parameter of its own or change where the link goes. | declared — 23 url attributes carrying a template expression, of 23 sink sites asserted: Every url attribute in this extension's templates takes its value whole from the link helper. |
1.2.3 |
No template expression is interpolated into a <script> element, so store data cannot end a string literal and start running. |
not met — 14 .twig files:extensions/gift_cards/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/gift_cards/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> elementextensions/gift_cards/src/admin/view/template/report/gift_cards.twig:83 — {{ filter_all }} is interpolated inside a <script> elementextensions/gift_cards/src/admin/view/template/report/gift_cards.twig:97 — {{ user_token }} is interpolated inside a <script> elementextensions/gift_cards/src/admin/view/template/report/gift_cards.twig:101 — {{ user_token }} 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 — 8 ways of building a statement, over 36 statements run and 13 values escaped:a statement handed over already built — the COUNT(*) PurgeCounts::total() runs is composed by its caller and arrives as a string (admin/model/customer/purge_counts.php). The one caller that composes one is Purge::counts() (admin/model/customer/purge.php), which interpolates a table name between backticks — and the names it folds over are the keys of this extension's own erasure declaration, so nothing a request carries can reach the statementa card row, keyed by an integer and dated by a canonical date — every statement over gift_cards_card interpolates integers and canonical dates and nothing else: a voucher_id and an order_id cast to (int) at the point of use, a list of order status ids each put through intval() as it is imploded, and an expiry date that has already been through Expiry::date() or Expiry::dateFrom() — both of which return a string matching Y-m-d or nothing at all, so what reaches $this->db->escape() cannot be a value a form posted. The table names, core's order and order_voucher included, are literals beside DB_PREFIX. The storefront model (catalog/model/gift_cards/card.php) and the admin model (admin/model/module/gift_cards.php) are both built this way, and the read of a card's expiry row is not built in either of them: both run the one statement Expiry::dateQuery() returns, which takes the prefix and an int and is asserted on by a unit testa settings mirror row, whose key and value are this extension's own — the two statements over gift_cards_memory (admin/model/module/gift_cards.php) write and read a key and a value that never come from a request unfiltered: every call site hands the writer Configuration::remembered(), which is an array_intersect_key against the declaration — so a key that is not one this extension declares cannot reach the statement at all, whatever was posted. The value is a merchant's own setting and goes through $this->db->escape(). The read interpolates nothinga card resolved from a code a stranger posted — Card::find() (catalog/model/gift_cards/card.php) is the one statement in this extension that interpolates a string straight off a public form. It goes through $this->db->escape(), it is the only thing in the statement that is not a literal, and it is compared to oc_voucher.code — a varchar(10) — inside a single-quoted literal. Nothing about the code reaches a column name, a table name or an ORDER BY. What the statement returns is four columns rather than SELECT *, so a card resolved by somebody who then fails the email challenge has already had the recipient's name, the buyer's address and the message left in the databaseevery card the installation has, read for the liability figure — the one statement in admin/model/report/gift_cards.php. It interpolates nothing at all: every table name is a literal beside DB_PREFIX, there is no WHERE clause, no filter, no sort a request chooses and no paging — the report is installation-wide and as at now, and the page it shows is sliced in PHP after the classifier has run. Nothing a request carries reaches itan attempt row, counted and written — the three statements in catalog/model/gift_cards/attempt.php. Every value they interpolate is either cast to (int) at the point of use (voucher_id, store_id) or produced by this extension's own code and escaped anyway: the two window and prune moments are date() output from Lookup, and the outcome is one of that class's three outcome constants. ip is the only one that comes from outside PHP — server[REMOTE_ADDR], which no request chooses — and it is truncated to the column's 40 characters and escaped. There is no statement here that takes a code or an email address, because neither is storeda read of core's own voucher rows, keyed by an integer — the two statements behind the admin order panel (admin/model/module/gift_cards.php): the cards one order sold, joined through oc_voucher so a card whose row core deleted drops out rather than reading wrongly, and one card's oc_voucher_history rows, the second of which is the statement Balance::historyQuery() returns and is shared byte for byte with the storefront's own read. Each interpolates a single integer — an order id core put in the view's own data, and a voucher_id that came off the row above it — beside literal table names and DB_PREFIX. Nothing a request typed reaches either, and both are reads: there is no write counterpart to them anywhere in this extension, because core stays the ledgera page of core's voucher rows, read for the API — the four statements in catalog/model/api/gift_cards.php. Every value they interpolate is an integer cast at the point of use — the caller's opencart_voucher_id, filter_opencart_order_id and the cursor's id, the page size, and the page's own voucher ids imploded into the history sum's IN (...) after each went through (int) — beside literal table names and DB_PREFIX. The order is a literal voucher_id DESC; a caller chooses no column, no direction and no sort. code, from_name, from_email, to_name, to_email and message are never in a select list, so they cannot reach the response by any path6 of the 36 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 — 64 .php files |
1.3.1 |
No screen binds a rich-text editor whose HTML this extension would then render back out, because nothing here sanitises HTML and no sanitiser ships with it. | checked — 14 .twig files |
1.3.2 |
Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided. |
checked — 64 .php files |
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 — 64 .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 — 9 of 22 routes set a Content-Type of their own: 5 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="gift-cards-outstanding- 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) 2 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 5 × none set, and no output written 1 × none set; a 404 status line where the merchant has switched the lookup off, and otherwise the store's default 1 × none set; it is a fragment loaded into the Reports screen 1 × none set; it is the table alone, for the filters and the paging to load into 1 × none set; it returns rendered markup rather than writing a response 4 × none set; the page goes out under whatever the front controller defaults to |
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 — 5 call sites in 2 templates, each declared with what it writes there: 2 × .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 — 64 .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 — 64 .php files |
3.5.1 |
Every route that changes something says what stands between it and a request another website caused a visitor's browser to make. | declared — 8 routes of 22 reaches a model write; the 8 admin ones among them stand behind the user_token core checks before dispatch, and 18 admin routes are gated that way in all:No storefront route of this extension reaches a model write. |
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 — 64 .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 — 64 .php files:extensions/gift_cards/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/gift_cards/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/gift_cards/src/admin/controller/module/gift_cards.php:257 — GiftCards::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/gift_cards/src/admin/controller/module/gift_cards.php:309 — GiftCards::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/gift_cards/src/admin/controller/module/gift_cards.php:480 — GiftCards::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/gift_cards/src/admin/controller/report/gift_cards.php:132 — GiftCards::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/gift_cards/src/admin/controller/report/gift_cards.php:170 — GiftCards::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/gift_cards/src/admin/controller/report/gift_cards.php:200 — GiftCards::uninstall() 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 — 9 of 22 routes set a Content-Type of their own: 5 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="gift-cards-outstanding- 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) 2 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 5 × none set, and no output written 1 × none set; a 404 status line where the merchant has switched the lookup off, and otherwise the store's default 1 × none set; it is a fragment loaded into the Reports screen 1 × none set; it is the table alone, for the filters and the paging to load into 1 × none set; it returns rendered markup rather than writing a response 4 × none set; the page goes out under whatever the front controller defaults to |
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 22 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 22 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 — 5 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 — 5 write sites, each declared with its file:line and pinned against the token stream both ways:3 × log file 2 × 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 — 14 .twig files |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | checked — 14 .twig files |
6.3.2 |
No credential is written into the source — no default account, and no password or key a reader of the shipped files could use. | checked — 64 .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 — 22 routes: 18 admin, 4 catalog, each declared beside the code |
8.2.1 |
An admin route that changes something tests the permission itself, in a condition that can refuse — and a route that only reads says so, standing behind the check OpenCart makes before dispatch. | declared — 18 admin routes: 11 pin a permission themselves, 0 at one same-class hop, 0 at two (the hop ceiling), 7 unpinned:extensions/gift_cards/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/gift_cards/customer/purge before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/module/gift_cards.php:178 — GiftCards::index() pins no permission of its own; core checks access on extension/gift_cards/module/gift_cards before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/report/gift_cards.php:85 — GiftCards::index() pins no permission of its own; core checks access on extension/gift_cards/report/gift_cards before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/report/gift_cards.php:218 — GiftCards::report() pins no permission of its own; core checks access on extension/gift_cards/report/gift_cards before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/report/gift_cards.php:275 — GiftCards::list() pins no permission of its own; core checks access on extension/gift_cards/report/gift_cards before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/report/gift_cards.php:296 — GiftCards::export() pins no permission of its own; core checks access on extension/gift_cards/report/gift_cards before dispatch. No model write is reachable from it.extensions/gift_cards/src/admin/controller/report/gift_cards.php:354 — GiftCards::getReport() pins no permission of its own; core checks access on extension/gift_cards/report/gift_cards 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 — 5 triples over 3 of 4 catalog routes; the admin half is one line on the shared page:extension/gift_cards/account/balance — whoever holds the card, signed in or not: One card per proved pair, and the pair is the whole of the authority — nothing on the page comes from a session. The history rows are then selected on that one voucher_id and stripped to date and amount by Balance::lines(), so a card spent on somebody else's order names nobody. Selected by post[code] and post[email]extension/gift_cards/account/balance — the address the request came from: Two counters, either of which refuses: attempts from one address, and attempts against one card where the code resolved. A miss is counted against the address alone, because a code that resolved to nothing has no card and remembering one would mean remembering the code. Selected by none — server[REMOTE_ADDR], which no request choosesextension/gift_cards/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/gift_cards/api/v1/card — a holder of one of core's oc_api credentials: The credential cannot be scoped, so what is withheld is withheld from everybody holding it: the card code is never selected, never a filter and never a sort key, and the sender, the recipient and the message are never selected either. A caller can learn what is left on a card and when it expires, and never the code that spends it. Selected by opencart_voucher_idextension/gift_cards/api/v1/card — a holder of one of core's oc_api credentials, walking the collection: filter_opencart_order_id is a convenience for a caller who has one order in mind, not a boundary: the same credential may simply leave it off. Selected by none — not a record; filter_opencart_order_id and the cursor are a page of a walk rather than a record selector |
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 — 5 distinct bounds, each named by the triple it scopes: bounded by nothing — an OpenCart API user opens every card in the installation, and a voucher in core has no store to narrow it by bounded by gift_cards_attempt.ip and gift_cards_attempt.voucher_id, each counted over the same window 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 oc_voucher.code with status = 1, then a case-insensitive comparison of post[email] against that row's to_email |
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 1 bearer-secret surface |
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 — 64 .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 — 1 bearer-secret surface, from core — never from anything inside the secret presented:system/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 — 0 surfaces of 1 bearer-secret surface 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 — 64 .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 — 64 .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 — 64 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | checked — 64 .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 — 64 .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. | declared — 0 bearer-secret surfaces of 1 travel in a URL |
14.3.1 |
Nothing is left behind in the browser's own storage for the next person at that computer to read. | checked — 14 .twig files |
15.2.1 |
The extension bundles no third-party library, so there is nothing inside it for you to keep patched other than our own code. | checked — 64 .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 — 18 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:module_gift_cards_status — the module_gift_cards settingmodule_gift_cards_validity_months — the module_gift_cards setting, cast to (int) by the controller before it reaches the template — so what the number input is rendered with is a number whatever the setting table came to holdexpires_on — the gift_cards_card row for the voucher whose form this is, put through Expiry::date() and then htmlspecialchars() by the listener that splices the field in — admin Twig has autoescape off in both releases, which is why the escaping is at the call site and not in the partiallegal_floors_warns — the same module_gift_cards setting, put through LegalFloors::warns() — so what the template renders is a boolean rather than the number, and it decides one Bootstrap class on an element whose text is a constant in this repository. The rows beside it (legal_floors, text_not_exhaustive, text_floor_source) come from LegalFloors and the language file and carry nothing the store holdsmodule_gift_cards_lookup — the module_gift_cards settingreport_gift_cards_status — the report_gift_cards settingreport_gift_cards_sort_order — the report_gift_cards settingheadline — the outstanding-liability figure, summed from oc_voucher and oc_voucher_history by Liability::figures() and already formatted by $this->currency->format() in the controllerheadline_count — how many cards that figure is over, an integer counted by Liability::figures()companions — the four figures beside the headline: each one's name, its placement sentence and the reason for it come from this extension's own language file, and its amount and count from Liability::figures() over the same two core tables, already formatted in the controllercards — one page of the oc_voucher rows any liability figure counts — the card code, the recipient's name, the purchasing order id, two amounts already formatted by $this->currency->format(), this extension's own expiry date, the five booleans the row's markers are decided from and the placement sentence out of this extension's own language file. The recipient's email address, the buyer's name and address, and the card's message are not selected by the report's model, so the subtree cannot carry themlist — this extension's own rendered table of the cards behind the figureresults — the paging sentence out of this extension's own language file, with four integers counted in the controllercard — one oc_voucher row a visitor proved with its code and its to_email, plus its oc_voucher_history rows — every figure already formatted by $this->currency->format() in the controller, and every history line already reduced to a date and an amount by Balance::lines(). The recipient's name, the buyer's name, the buyer's address, the message and the order each redemption sat on are not selected, so the subtree cannot carry themcode — the card code the visitor posted, echoed back into the form so a mistyped address is not a retyped code. Printed as it arrived: OpenCart runs Twig with autoescape off, and the value is safe because Request::clean() entity-encoded the post before this page read it; the address is deliberately not echoedbalance_link — this extension's own route, built by $this->url->link() in the listener that splices the link into core's footer — so the only part of it the store decides is config_language, and Url::link() has already put it into the & form an HTML attribute wants. It is deliberately not escaped a second time, for the reason core prints its own footer links the same way. The label beside it is this extension's own heading_title, escaped at that call site because storefront Twig has autoescape off in both releasestext_validity — the module_gift_cards validity period, cast to (int) and concatenated into a sentence out of this extension's own language file by the listener that splices it into core's gift card purchase form — then put through htmlspecialchars() there, because storefront Twig has autoescape off in both releases. The number is the only part of it the store decides, and it is an integer by the time it is anywhere near the stringapi_panel — settings and database — whether the API is on, and core's own oc_api rows |
16.2.5 |
No log line names a credential — no token, secret, signature or password is written into the file the error log screen renders. | checked — 64 .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 — 64 .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 — 64 .php files |