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 — 0 bearer-secret surfaces over 1 mint, compare and refuse site (0 × mint, 0 × constant-time compare, 1 × compare, 0 × refusal); 5 further entries say what the derivation reached that is not a secret |
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: 5 sink sites asserted here, 2 not admitted; attested over the inventory: 100 store-derived subtrees over 8 templates; unverified beyond it: everything else:extensions/seo/src/system/library/xml_sitemap/sitemap.php:108 — <lastmod> is hand-built XML taking a value with no escaper standing in front of itextensions/seo/src/system/library/xml_sitemap/sitemap_index.php:43 — <lastmod> is hand-built XML taking a value with no escaper standing in front of itgenerates — the same setting group: which entity types the store being edited generates for. Rendered by admin/view/template/seo/urls_and_meta.twig.separator — the same setting group: what joins the names of a category path. Rendered by admin/view/template/seo/urls_and_meta.twig.store_id — the store the form is editing, taken from the query string as an integer. Rendered by admin/view/template/seo/urls_and_meta.twig.setting_stores — core's own store rows, reduced to an id and a name for the selector saying which shop is being edited. Rendered by admin/view/template/seo/urls_and_meta.twig.module_seo_basics_status — the module_seo_basics setting. Rendered by admin/view/template/seo/urls_and_meta.twig.groups — the form, group by group: each field's saved template text out of the module_seo_basics settings, beside the label the language file gives it. Rendered by admin/view/template/seo/urls_and_meta.twig.batch — the saved batch size, out of the same setting group. Rendered by admin/view/template/seo/urls_and_meta.twig.canonical — the saved canonical setting. Rendered by admin/view/template/seo/urls_and_meta.twig.stores — the store's own name from config_name, followed by every row of core's store table. Rendered by admin/view/template/seo/urls_and_meta.twig.languages — every row of core's language table, through localisation/language. Rendered by admin/view/template/seo/urls_and_meta.twig.language_readout — the same language rows, reduced to the sentence saying which of them this screen resolved against. Rendered by admin/view/template/seo/urls_and_meta.twig.version_warning — the store's own OpenCart version, compared against this extension's compatibility floor. Rendered by admin/view/template/seo/urls_and_meta.twig.availability — core's own stock_status rows, each with what this extension reports it as. Rendered by admin/view/template/seo/markup.twig.availability_key — this extension's own setting key, as the name the availability rows post under. Rendered by admin/view/template/seo/markup.twig.availability_terms — this extension's own fixed list of schema.org availability terms. Rendered by admin/view/template/seo/markup.twig.store_id — the store the form is editing, taken from the query string as an integer. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_og_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_twitter_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_product_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_breadcrumb_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_organization_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_hreflang_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_default_language — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_site_name — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_handle — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_profiles — the module_seo_markup setting group, read one key at a time into $data under the setting's own name. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_card — the same setting group, narrowed to the card the emitter would actually use rather than the raw stored value. Rendered by admin/view/template/seo/markup.twig.module_seo_markup_image — the same setting group: the image path the store owner picked, as core's image manager stored it. Rendered by admin/view/template/seo/markup.twig.cards — the card names this extension offers, a constant list labelled from the language file. Rendered by admin/view/template/seo/markup.twig.languages — every row of core's language table, through localisation/language. Rendered by admin/view/template/seo/markup.twig.language_readout — the same language rows, reduced to the sentence saying which of them this screen resolved against. Rendered by admin/view/template/seo/markup.twig.site_name_placeholder — the store's own name, from config_name, shown as what the tags fall back to. Rendered by admin/view/template/seo/markup.twig.placeholder — core's no_image.png resized through tool/image. Rendered by admin/view/template/seo/markup.twig.thumb — the stored image path resized through tool/image, or placeholder where the file is not there. Rendered by admin/view/template/seo/markup.twig.list — the overrides panel, rendered: the override rows this extension has stored, the routes they may name, core's store rows and core's language rows. Rendered by admin/view/template/seo/markup.twig, admin/view/template/seo/markup_overrides.twig.version_warning — the store's own OpenCart version, compared against this extension's compatibility floor. Rendered by admin/view/template/seo/markup.twig.user_token — the admin session token, read off $this->session->data and handed to the template so its script can call core's product autocomplete. Rendered by admin/view/template/seo/alt_text.twig.module_seo_alt_text_batch — the same setting group: how many products one request of a run goes through. Rendered by admin/view/template/seo/alt_text.twig.store_id — the store the form is editing, taken from the query string as an integer. Rendered by admin/view/template/seo/alt_text.twig.module_seo_alt_text_status — the module_seo_alt_text setting. Rendered by admin/view/template/seo/alt_text.twig.version_warning — the store's own OpenCart version, compared against this extension's compatibility floor. Rendered by admin/view/template/seo/alt_text.twig.languages — every row of core's language table, through localisation/language. Rendered by admin/view/template/seo/alt_text.twig.language_readout — the same language rows, reduced to the sentence saying which of them this screen resolved against. Rendered by admin/view/template/seo/alt_text.twig.product_id — the product_id in the query string, kept only when core's catalog/product model returned a row for it. Rendered by admin/view/template/seo/alt_text.twig.product_name — that product's name, from core's catalog/product model. Rendered by admin/view/template/seo/alt_text.twig.product_image — that product's image path, from the same row. Rendered by admin/view/template/seo/alt_text.twig.alt — the descriptions this extension has stored for that product's main image, one per language, from its own table. Rendered by admin/view/template/seo/alt_text.twig.length — the column's own length, a constant of this extension rather than store data; listed because the field it limits is store data. Rendered by admin/view/template/seo/alt_text.twig.descriptions — the most recent stored descriptions, each joined to the product name core's model answers for its product_id. Rendered by admin/view/template/seo/alt_text.twig.total — how many descriptions this extension's table holds. Rendered by admin/view/template/seo/alt_text.twig.shown — how many of them the screen lists, a constant of this extension. Rendered by admin/view/template/seo/alt_text.twig.rewrite_applies — the store's SEO URL setting, read to say whether the storefront rewrites URLs at all. Rendered by admin/view/template/seo/alt_text.twig.product_rewrite_applies — the same setting narrowed to the product route. Rendered by admin/view/template/seo/alt_text.twig.placeholder_list — the placeholder names this extension's template language accepts, a constant. Rendered by admin/view/template/seo/alt_text.twig.copy_panel — the copy panel rendered for the language the screen is open on: this extension's stored templates for that language, and the language rows they are named by. Rendered by admin/view/template/seo/alt_text.twig.template_unknown — the placeholders in the operator's stored template that this extension does not recognise. Rendered by admin/view/template/seo/alt_text.twig.template_flat — whether that stored template distinguishes one image from another. Rendered by admin/view/template/seo/alt_text.twig.stores — the store's own name from config_name, followed by every row of core's store table. Rendered by admin/view/template/seo/alt_text.twig.unfinished — what the last generate run left behind, read out of the setting this extension stores its position in. Rendered by admin/view/template/seo/alt_text.twig.can_modify — whether the signed-in administrator holds modify on this route; the screen hides its buttons on it, and every route that writes checks it again for itself. Rendered by admin/view/template/seo/alt_text.twig.module_seo_redirects_status — the module_seo_redirects setting. Rendered by admin/view/template/seo/redirects.twig.module_seo_redirects_cap — the same setting group: how many misses the log is allowed to hold. Rendered by admin/view/template/seo/redirects.twig.module_seo_redirects_capture — the same setting group: whether a changed keyword is captured as a redirect. Rendered by admin/view/template/seo/redirects.twig.module_seo_redirects_response — the same setting group: the response code a new rule for the store being edited starts with. Rendered by admin/view/template/seo/redirects.twig.store_id — the store the form is editing, taken from the query string as an integer. Rendered by admin/view/template/seo/redirects.twig.stores — core's own store rows, reduced to an id and a name for the selector saying which shop is being edited. Rendered by admin/view/template/seo/redirects.twig.defaults — the same setting group, read once per store: which response code each shop's new rules start at. Rendered by admin/view/template/seo/redirects_misses.twig, admin/view/template/seo/redirects_redirects.twig.list — both panels, rendered: the miss rows and the redirect rows this extension has logged — each carrying the path a visitor asked for and the referrer their browser sent — beside core's store rows. Rendered by admin/view/template/seo/redirects.twig, admin/view/template/seo/redirects_misses.twig, admin/view/template/seo/redirects_redirects.twig.language_readout — core's language rows, reduced to the sentence saying which of them this screen resolved against. Rendered by admin/view/template/seo/redirects.twig.version_warning — the store's own OpenCart version, compared against this extension's compatibility floor. Rendered by admin/view/template/seo/redirects.twig.includes — the same setting group: which entity types the store being edited carries in its sitemap. Rendered by admin/view/template/seo/xml_sitemap.twig.module_seo_xml_sitemap_prefix — the same setting group: what every published file's name begins with. Rendered by admin/view/template/seo/xml_sitemap.twig.module_seo_xml_sitemap_index — the same setting group: what the default store's index file is called. Rendered by admin/view/template/seo/xml_sitemap.twig.store_id — the store the form is editing, taken from the query string as an integer. Rendered by admin/view/template/seo/xml_sitemap.twig.stores — core's own store rows, reduced to an id and a name for the selector saying which shop is being edited. Rendered by admin/view/template/seo/xml_sitemap.twig.module_seo_xml_sitemap_status — the module_seo_xml_sitemap setting. Rendered by admin/view/template/seo/xml_sitemap.twig.module_seo_xml_sitemap_cycle — the same setting group: how often a scheduled run is due. Rendered by admin/view/template/seo/xml_sitemap.twig.seo_xml_sitemap_rules — this extension's own setting rows: the rules saying which stores, languages and entity types a run covers, as the store owner wrote them. Rendered by admin/view/template/seo/xml_sitemap.twig.cycles — the cycles this extension offers, a constant list labelled from the language file. Rendered by admin/view/template/seo/xml_sitemap.twig.robots_path — where robots.txt would sit under this store's own DIR_OPENCART. Rendered by admin/view/template/seo/xml_sitemap.twig.robots_served — whether a file is there at that path. Rendered by admin/view/template/seo/xml_sitemap.twig.robots_problem — why that path cannot be written, read off the filesystem — a missing directory, or one that is not writable. Rendered by admin/view/template/seo/xml_sitemap.twig.sitemap_path — where the sitemap index sits under this store's own DIR_OPENCART. Rendered by admin/view/template/seo/xml_sitemap.twig.sitemap_exists — whether a file is there at that path. Rendered by admin/view/template/seo/xml_sitemap.twig.sitemap_generated — the modification time of the file on disk. Rendered by admin/view/template/seo/xml_sitemap.twig.sitemap_urls — how many URLs the last run wrote, read out of this extension's own setting rows. Rendered by admin/view/template/seo/xml_sitemap.twig.sitemap_problem — why the sitemap path cannot be written, read off the filesystem. Rendered by admin/view/template/seo/xml_sitemap.twig.indexes — the sitemap files on disk under the web root, each with its name and size. Rendered by admin/view/template/seo/xml_sitemap.twig.begin — how many URLs a run has to get through before the first file is published, computed from the saved rules against the catalog. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_ever — whether a scheduled run has ever happened, out of this extension's stored run record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_date — when the last scheduled run finished, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_started — when it started, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_urls — how many URLs it wrote, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_succeeded — whether it finished, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_unfinished — whether it stopped part-way, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_failed — whether it failed, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.cron_error — what it failed with, from the same record. Rendered by admin/view/template/seo/xml_sitemap.twig.language_readout — core's language rows, reduced to the sentence saying which of them this screen resolved against. Rendered by admin/view/template/seo/xml_sitemap.twig.version_warning — the store's own OpenCart version, compared against this extension's compatibility floor. Rendered by admin/view/template/seo/xml_sitemap.twig.Unverified beyond it: the other 681 of 730 template expressions in 15 templates, and any $data subtree nobody enumerated. The inventory is an inventory and not a bound: completeness over the whole expression surface is unverifiable, so this residual is permanent, and it is published rather than left to be inferred from what is missing. |
1.2.2 |
A URL a template builds for itself, rather than taking one whole from the link helper, has every value in it URL-encoded — so nothing a store holds can add a parameter of its own or change where the link goes. | not met — 44 url attributes carrying a template expression, of 49 sink sites asserted:extensions/seo/src/admin/view/template/seo/alt_text.twig:121 — src="language/{{ language.code }}/{{ language.code }}.png" composes a URL in the template rather than taking one whole from the link helper, and not every value in it is URL-encodedextensions/seo/src/admin/view/template/seo/alt_text.twig:225 — href="{{ edit }}&product_id={{ description.product_id }}" composes a URL in the template rather than taking one whole from the link helper, and not every value in it is URL-encoded |
1.2.3 |
No template expression is interpolated into a <script> element, so store data cannot end a string literal and start running. |
not met — 15 .twig files:extensions/seo/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/alt_text.twig:263 — {{ import }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/alt_text.twig:310 — {{ generate }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/alt_text.twig:363 — {{ reset }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/alt_text.twig:386 — {{ user_token }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/alt_text.twig:403 — {{ edit }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/markup_overrides.twig:135 — {{ import }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/markup_overrides.twig:158 — {{ reload }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/redirects_redirects.twig:95 — {{ import }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/redirects_redirects.twig:118 — {{ reload }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/urls_and_meta.twig:267 — {{ preview }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/urls_and_meta.twig:316 — {{ generate }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/urls_and_meta.twig:327 — {{ confirm_field }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/urls_and_meta.twig:327 — {{ confirmed }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/urls_and_meta.twig:332 — {{ regenerate }} is interpolated inside a <script> elementextensions/seo/src/admin/view/template/seo/xml_sitemap.twig:207 — {{ generate }} 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 — 20 ways of building a statement, over 45 statements run and 51 values escaped:escaped string literal — a string goes through $this->db->escape() and is interpolated between single quotes: the keyword, the seo_url key and value, and the shortened meta text (port/database_store.php)interpolated identifier — two shapes. A table name built from the store's DB_PREFIX and a suffix, and — in the meta update — a table and a column name taken from this extension's own fixed map of entity types, interpolated between backticks. Neither is ever built from request inputinterpolated integer — an int-typed parameter concatenated bare, unquoted and unescaped — store_id, language_id and the row identifierswhole-clause concatenation — the LIMIT clause, built whole from an int and appended, or left off when there is no limit (port/database_store.php)escaped string literal — a string goes through $this->db->escape() and is interpolated between single quotes: the route, the page key, the canonical, and the seo_url key and value (port/database_overrides.php, catalog/model/markup/alternates.php)interpolated identifier — a table name built from the store's DB_PREFIX and a suffix constant of this extension's, interpolated between backticks; no identifier is ever built from request input. The CREATE TABLE is built whole by system/library/markup/schema.php out of that constant list and handed to $this->db->query() already assembled, which is why a statement count reads it as handed overinterpolated integer — an int-typed parameter or a (int) cast concatenated into the statement — store_id, language_id, stock_status_id and the row limitwhole-clause concatenation — two shapes. A whole WHERE built by a private page() and concatenated onto a DELETE or SELECT, and an OR list joined with implode() from one escaped pair per language (port/database_overrides.php, catalog/model/markup/alternates.php); plus the LIMIT, appended or left offescaped string literal — a string goes through $this->db->escape() and is interpolated between single quotes: the image path, the theme route, and the table name in SHOW TABLES LIKE (port/database_alt_text.php, template_source.php)interpolated identifier — a table name built from the store's DB_PREFIX and a suffix constant of this extension's, interpolated between backticks; no identifier is ever built from request input. The CREATE TABLE is built whole by system/library/alt_text/schema.php out of that constant list and handed to $this->db->query() already assembled, which is why a statement count reads it as handed overinterpolated integer — an int-typed parameter concatenated bare, unquoted and unescaped — product_id, language_id, store_id and the row limitwhole-clause concatenation — a clause built and appended whole: the IN (...) store list, the LIMIT, and the ANDstatus= '1' a theme table that has that column gets (port/database_alt_text.php, template_source.php)escaped string literal — a string goes through $this->db->escape() and is interpolated between single quotes: the path a visitor asked for, the referrer their browser sent, the redirect target, the timestamp, the origin, and the seo_url keyword and key (port/database_log.php)interpolated identifier — a table name built from the store's DB_PREFIX and a suffix constant of this extension's, interpolated between backticks; no identifier is ever built from request input. The CREATE TABLE and the ADD COLUMN are built whole by system/library/redirects/schema.php out of that constant list and handed to $this->db->query() already assembled, which is why a statement count reads them as handed overinterpolated integer — an int-typed parameter concatenated bare, unquoted and unescaped — store_id, the HTTP response code, and the row limitwhole-clause concatenation — a clause built and appended whole, or left off: the ANDstore_id` when one store is asked for, and theLIMITwhen there is one (port/database_log.php)<br>escaped string literal— a string goes through$this->db->escape()and is interpolated between single quotes: theseo_urlkey, and the setting code, key and value this extension writes (port/database_catalog.php,port/database_scopes.php,port/database_settings.php)<br>interpolated identifier— a table name built from the store'sDB_PREFIXand a suffix, interpolated between backticks; no identifier is ever built from request input<br>interpolated integer— anint-typed parameter concatenated bare, unquoted and unescaped —store_id,language_idand the row limit a batch reads under<br>whole-clause concatenation— theLIMITclause, built whole from anintand appended, or left off when there is no limit (port/database_catalog.php`)14 of the 45 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 — 135 .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 — 15 .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 — 135 .php files:extensions/seo/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 — 135 .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 — 29 of 54 routes set a Content-Type of their own: 20 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-alt-text.csv" and Cache-Control: no-store 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-broken-urls.csv" and Cache-Control: no-store. The two visitor-typed columns are defused by PathExport: the path is written behind a leading /, which the import trims, and the referrer gains a leading ' when it starts with =, +, -, @, a tab or a carriage return1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-markup-overrides.csv" and Cache-Control: no-store 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-redirects.csv" and Cache-Control: no-store 1 × application/json, with no charset (admin/controller/module/seo.php:195) 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) 11 × none set, and no output written 6 × none set; the page goes out under whatever the front controller defaults to 2 × none set; the partial goes out under whatever the front controller defaults to 1 × none — install() returns nothing and sets no header 2 × none — nothing sets a Content-Type, so the store's default stands 1 × none — uninstall() returns nothing and sets no header 1 × none. A refused call gets the status line HTTP/1.1 404 Not Found and a short body, with no Content-Type; an admitted one writes files and sends nothing. This extension publishes by writing into the web root for the server to serve, so it has no response surface carrying a sitemap at all1 × none. The 404 body is plain text with no type set; an admitted run writes files into the web root and prints its result to the terminal |
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 — 40 call sites in 7 templates, each declared with what it writes there: 18 × .append(1 × .html(21 × .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 — 135 .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 — 135 .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 — 25 routes of 54 reaches a model write; the 25 admin ones among them stand behind the user_token core checks before dispatch, and 51 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 — 135 .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 — 135 .php files:extensions/seo/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/seo/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/seo/src/admin/controller/module/seo.php:220 — Seo::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/module/seo.php:275 — Seo::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/module/seo.php:371 — Seo::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/alt_text.php:350 — AltText::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/alt_text.php:523 — AltText::generate() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/alt_text.php:601 — AltText::reset() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/alt_text.php:812 — AltText::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/alt_text.php:919 — AltText::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/markup.php:400 — Markup::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/markup.php:1071 — Markup::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/markup.php:1166 — Markup::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/redirects.php:208 — Redirects::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/redirects.php:727 — Redirects::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/redirects.php:813 — Redirects::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/urls_and_meta.php:317 — UrlsAndMeta::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/urls_and_meta.php:481 — UrlsAndMeta::generate() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/urls_and_meta.php:504 — UrlsAndMeta::regenerate() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/urls_and_meta.php:1247 — UrlsAndMeta::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/urls_and_meta.php:1330 — UrlsAndMeta::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/xml_sitemap.php:291 — XmlSitemap::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/xml_sitemap.php:419 — XmlSitemap::generate() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/xml_sitemap.php:520 — XmlSitemap::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/seo/src/admin/controller/seo/xml_sitemap.php:642 — XmlSitemap::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 — 29 of 54 routes set a Content-Type of their own: 20 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-alt-text.csv" and Cache-Control: no-store 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-broken-urls.csv" and Cache-Control: no-store. The two visitor-typed columns are defused by PathExport: the path is written behind a leading /, which the import trims, and the referrer gains a leading ' when it starts with =, +, -, @, a tab or a carriage return1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-markup-overrides.csv" and Cache-Control: no-store 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="seo-redirects.csv" and Cache-Control: no-store 1 × application/json, with no charset (admin/controller/module/seo.php:195) 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) 11 × none set, and no output written 6 × none set; the page goes out under whatever the front controller defaults to 2 × none set; the partial goes out under whatever the front controller defaults to 1 × none — install() returns nothing and sets no header 2 × none — nothing sets a Content-Type, so the store's default stands 1 × none — uninstall() returns nothing and sets no header 1 × none. A refused call gets the status line HTTP/1.1 404 Not Found and a short body, with no Content-Type; an admitted one writes files and sends nothing. This extension publishes by writing into the web root for the server to serve, so it has no response surface carrying a sitemap at all1 × none. The 404 body is plain text with no type set; an admitted run writes files into the web root and prints its result to the terminal |
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 — 3 upload surfaces across 54 routes:extension/seo/seo/markup.import — $this->request->files['file'], a CSV of overrides. The temporary name is read where it lies; nothing is moved, and no name from the upload reaches a pathextension/seo/seo/alt_text.import — $this->request->files['file'], a CSV. The temporary name is put through is_uploaded_file() and read where it lies; nothing is moved, and no name from the upload reaches a pathextension/seo/seo/redirects.import — $this->request->files['file'], a CSV of redirects. The temporary name is read where it lies; nothing is moved, and no name from the upload reaches a path |
5.2.2 |
An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands. | declared — 3 upload surfaces across 54 routes:extension/seo/seo/markup.import — $this->request->files['file'], a CSV of overrides. The temporary name is read where it lies; nothing is moved, and no name from the upload reaches a pathextension/seo/seo/alt_text.import — $this->request->files['file'], a CSV. The temporary name is put through is_uploaded_file() and read where it lies; nothing is moved, and no name from the upload reaches a pathextension/seo/seo/redirects.import — $this->request->files['file'], a CSV of redirects. The temporary name is read where it lies; nothing is moved, and no name from the upload reaches a path |
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 — 19 write sites, 7 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:495system/library/xml_sitemap/web_root_file.php:174 — file_put_contents() with LOCK_EX over the whole file, at system/library/xml_sitemap/web_root_file.php:174. The paths are fixed names under DIR_OPENCART — robots.txt and sitemap*.xml — and no part of a name comes from a requestsystem/library/xml_sitemap/web_root_file.php:200 — file_put_contents() with FILE_APPEND | LOCK_EX in the same class, at system/library/xml_sitemap/web_root_file.php:200: what each batch of a run adds to the partial file it is buildingsystem/library/xml_sitemap/web_root_file.php:226 — rename() in the same class, at system/library/xml_sitemap/web_root_file.php:226, moving a finished .part file over the name a crawler asks for — atomic on one filesystem, which is why a run publishes this waysystem/library/xml_sitemap/web_root_file.php:248 — unlink() in the same class, at system/library/xml_sitemap/web_root_file.php:248: what uninstalling does to a robots.txt this extension made, and to a sitemap nobody else can regenerate |
5.3.2 |
Every path this extension writes to is written down beside the code, with where the name in it came from. | declared — 19 write sites, each declared with its file:line and pinned against the token stream both ways:1 × append 1 × delete 3 × log file 1 × rename 12 × stream 1 × whole-file write |
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 — 15 .twig files |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | checked — 15 .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 — 135 .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 — 54 routes: 51 admin, 3 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 — 51 admin routes: 38 pin a permission themselves, 2 at one same-class hop, 0 at two (the hop ceiling), 11 unpinned:extensions/seo/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/seo/customer/purge before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/module/seo.php:144 — Seo::index() pins no permission of its own; core checks access on extension/seo/module/seo before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/module/seo.php:371 — Seo::uninstall() pins no permission of its own; core checks access on extension/seo/module/seo before dispatch. A model write is reachable from it.extensions/seo/src/admin/controller/seo/alt_text.php:183 — AltText::index() pins no permission of its own; core checks access on extension/seo/seo/alt_text before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/markup.php:265 — Markup::index() pins no permission of its own; core checks access on extension/seo/seo/markup before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/markup.php:517 — Markup::list() pins no permission of its own; core checks access on extension/seo/seo/markup before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/redirects.php:134 — Redirects::index() pins no permission of its own; core checks access on extension/seo/seo/redirects before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/redirects.php:303 — Redirects::list() pins no permission of its own; core checks access on extension/seo/seo/redirects before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/urls_and_meta.php:150 — UrlsAndMeta::index() pins no permission of its own; core checks access on extension/seo/seo/urls_and_meta before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/urls_and_meta.php:421 — UrlsAndMeta::preview() pins no permission of its own; core checks access on extension/seo/seo/urls_and_meta before dispatch. No model write is reachable from it.extensions/seo/src/admin/controller/seo/xml_sitemap.php:108 — XmlSitemap::index() pins no permission of its own; core checks access on extension/seo/seo/xml_sitemap 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 — 2 triples over 2 of 3 catalog routes; the admin half is one line on the shared page:extension/seo/cli/xml_sitemap — whoever can run PHP on the server: Reached through extension/seo/seo_xml_sitemap.php, and refused outright to anything that is not a terminal. It partitions nothing per identity because there is one identity: the shell. What it publishes is what the store's own saved rules cover, which is what the scheduled route publishes too — the two doors share one model method. Selected by none — not a record, and the command takes no arguments at all: anything after the filename is refusedextension/seo/cron/xml_sitemap — none — nothing dispatches this on behalf of a customer or an administrator; core's scheduler dispatches it on behalf of the store: A scheduled run covers whatever the store's own saved rules cover, and a caller cannot narrow or widen it, because a caller supplies nothing the run reads. Selected by none — not a record. The method reads no request key whatsoever; its only argument is the cron_id core's dispatcher passes |
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 — 2 distinct bounds, each named by the triple it scopes: bounded by the operating-system account the file is run as bounded by the stores and languages the saved rules name, read from the setting table rather than from the request |
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 0 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 — 135 .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 — 0 bearer-secret surfaces, from no source at all — never from anything inside the secret presented: This extension accepts no bearer secret. |
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 0 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 — 135 .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 — 135 .php files |
11.4.1 |
Every hash this extension computes is written down with what it is for, so a hash naming a cache entry is not read as one standing in front of a secret. | declared — 1 hash use over 1 call to 1 hash function:the generate run signature — sha1() over a JSON encoding of the run's templates, scope and overwrite flag, stored beside the run's position so the next request is matched to the same run. It is an identity for a resumable run and nothing authenticates on it |
12.1.1 |
No outbound request asks for a TLS version below 1.2, and none pins itself to one at all. | checked — 135 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | checked — 135 .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 — 135 .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 0 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 — 15 .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 — 135 .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 — 100 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:generates — the same setting group: which entity types the store being edited generates forseparator — the same setting group: what joins the names of a category pathstore_id — the store the form is editing, taken from the query string as an integersetting_stores — core's own store rows, reduced to an id and a name for the selector saying which shop is being editedmodule_seo_basics_status — the module_seo_basics settinggroups — the form, group by group: each field's saved template text out of the module_seo_basics settings, beside the label the language file gives itbatch — the saved batch size, out of the same setting groupcanonical — the saved canonical settingstores — the store's own name from config_name, followed by every row of core's store tablelanguages — every row of core's language table, through localisation/languagelanguage_readout — the same language rows, reduced to the sentence saying which of them this screen resolved againstversion_warning — the store's own OpenCart version, compared against this extension's compatibility flooravailability — core's own stock_status rows, each with what this extension reports it asavailability_key — this extension's own setting key, as the name the availability rows post underavailability_terms — this extension's own fixed list of schema.org availability termsstore_id — the store the form is editing, taken from the query string as an integermodule_seo_markup_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_og_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_twitter_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_product_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_breadcrumb_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_organization_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_hreflang_status — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_default_language — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_site_name — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_handle — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_profiles — the module_seo_markup setting group, read one key at a time into $data under the setting's own namemodule_seo_markup_card — the same setting group, narrowed to the card the emitter would actually use rather than the raw stored valuemodule_seo_markup_image — the same setting group: the image path the store owner picked, as core's image manager stored itcards — the card names this extension offers, a constant list labelled from the language filelanguages — every row of core's language table, through localisation/languagelanguage_readout — the same language rows, reduced to the sentence saying which of them this screen resolved againstsite_name_placeholder — the store's own name, from config_name, shown as what the tags fall back toplaceholder — core's no_image.png resized through tool/imagethumb — the stored image path resized through tool/image, or placeholder where the file is not therelist — the overrides panel, rendered: the override rows this extension has stored, the routes they may name, core's store rows and core's language rowsversion_warning — the store's own OpenCart version, compared against this extension's compatibility flooruser_token — the admin session token, read off $this->session->data and handed to the template so its script can call core's product autocompletemodule_seo_alt_text_batch — the same setting group: how many products one request of a run goes throughstore_id — the store the form is editing, taken from the query string as an integermodule_seo_alt_text_status — the module_seo_alt_text settingversion_warning — the store's own OpenCart version, compared against this extension's compatibility floorlanguages — every row of core's language table, through localisation/languagelanguage_readout — the same language rows, reduced to the sentence saying which of them this screen resolved againstproduct_id — the product_id in the query string, kept only when core's catalog/product model returned a row for itproduct_name — that product's name, from core's catalog/product modelproduct_image — that product's image path, from the same rowalt — the descriptions this extension has stored for that product's main image, one per language, from its own tablelength — the column's own length, a constant of this extension rather than store data; listed because the field it limits is store datadescriptions — the most recent stored descriptions, each joined to the product name core's model answers for its product_idtotal — how many descriptions this extension's table holdsshown — how many of them the screen lists, a constant of this extensionrewrite_applies — the store's SEO URL setting, read to say whether the storefront rewrites URLs at allproduct_rewrite_applies — the same setting narrowed to the product routeplaceholder_list — the placeholder names this extension's template language accepts, a constantcopy_panel — the copy panel rendered for the language the screen is open on: this extension's stored templates for that language, and the language rows they are named bytemplate_unknown — the placeholders in the operator's stored template that this extension does not recognisetemplate_flat — whether that stored template distinguishes one image from anotherstores — the store's own name from config_name, followed by every row of core's store tableunfinished — what the last generate run left behind, read out of the setting this extension stores its position incan_modify — whether the signed-in administrator holds modify on this route; the screen hides its buttons on it, and every route that writes checks it again for itselfmodule_seo_redirects_status — the module_seo_redirects settingmodule_seo_redirects_cap — the same setting group: how many misses the log is allowed to holdmodule_seo_redirects_capture — the same setting group: whether a changed keyword is captured as a redirectmodule_seo_redirects_response — the same setting group: the response code a new rule for the store being edited starts withstore_id — the store the form is editing, taken from the query string as an integerstores — core's own store rows, reduced to an id and a name for the selector saying which shop is being editeddefaults — the same setting group, read once per store: which response code each shop's new rules start atlist — both panels, rendered: the miss rows and the redirect rows this extension has logged — each carrying the path a visitor asked for and the referrer their browser sent — beside core's store rowslanguage_readout — core's language rows, reduced to the sentence saying which of them this screen resolved againstversion_warning — the store's own OpenCart version, compared against this extension's compatibility floorincludes — the same setting group: which entity types the store being edited carries in its sitemapmodule_seo_xml_sitemap_prefix — the same setting group: what every published file's name begins withmodule_seo_xml_sitemap_index — the same setting group: what the default store's index file is calledstore_id — the store the form is editing, taken from the query string as an integerstores — core's own store rows, reduced to an id and a name for the selector saying which shop is being editedmodule_seo_xml_sitemap_status — the module_seo_xml_sitemap settingmodule_seo_xml_sitemap_cycle — the same setting group: how often a scheduled run is dueseo_xml_sitemap_rules — this extension's own setting rows: the rules saying which stores, languages and entity types a run covers, as the store owner wrote themcycles — the cycles this extension offers, a constant list labelled from the language filerobots_path — where robots.txt would sit under this store's own DIR_OPENCARTrobots_served — whether a file is there at that pathrobots_problem — why that path cannot be written, read off the filesystem — a missing directory, or one that is not writablesitemap_path — where the sitemap index sits under this store's own DIR_OPENCARTsitemap_exists — whether a file is there at that pathsitemap_generated — the modification time of the file on disksitemap_urls — how many URLs the last run wrote, read out of this extension's own setting rowssitemap_problem — why the sitemap path cannot be written, read off the filesystemindexes — the sitemap files on disk under the web root, each with its name and sizebegin — how many URLs a run has to get through before the first file is published, computed from the saved rules against the catalogcron_ever — whether a scheduled run has ever happened, out of this extension's stored run recordcron_date — when the last scheduled run finished, from the same recordcron_started — when it started, from the same recordcron_urls — how many URLs it wrote, from the same recordcron_succeeded — whether it finished, from the same recordcron_unfinished — whether it stopped part-way, from the same recordcron_failed — whether it failed, from the same recordcron_error — what it failed with, from the same recordlanguage_readout — core's language rows, reduced to the sentence saying which of them this screen resolved againstversion_warning — the store's own OpenCart version, compared against this extension's compatibility floor |
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 — 135 .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 — 135 .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 — 135 .php files |