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 — 1 bearer-secret surface over 3 mint, compare and refuse sites (2 × mint, 1 × constant-time compare, 0 × compare, 0 × refusal); 2 further entries say what the derivation reached that is not a secret: the HTTP Basic key on the api/ routes — core's own oc_api credential, read at system/library/api_gateway.php:850-881 from HTTP_AUTHORIZATION, REDIRECT_HTTP_AUTHORIZATION, PHP_AUTH_USER/PHP_AUTH_PW and apache_request_headers() in that order, and compared at api_gateway.php:948 with hash_equals against a dummy of equal length where the username is unknown. Compared in constant time at system/library/api_gateway.php:948. No refusal of an unset value sits in this extension's own code. Minted by core, so no entropy is asserted here: nothing in this baseline rests on core's own token helper. |
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: 66 store-derived subtrees over 12 templates; unverified beyond it: everything else:module_product_bundles_status — setting. Rendered by admin/view/template/module/product_bundles.twig.module_product_bundles_module_limit — setting, held to MODULE_LIMIT_MIN..MODULE_LIMIT_MAX by Configuration::moduleLimit(). Rendered by admin/view/template/module/product_bundles.twig.version_warning — the store's OpenCart release, through Compatibility. Rendered by admin/view/template/module/product_bundles.twig.language_readout — rendered partial over oc_language. Rendered by admin/view/template/module/product_bundles.twig.resolution — rendered from oc_language, through LanguageReadout. Rendered by admin/view/template/module/language_readout.twig.user_token — the admin session token, rendered into this screen's own JavaScript URLs. Rendered by admin/view/template/catalog/bundle.twig, admin/view/template/catalog/bundle_form.twig, admin/view/template/sale/component.twig.filter_name — request — get[filter_name]. Rendered by admin/view/template/catalog/bundle.twig.bundles — database — product_bundles_bundle rows with their descriptions, and on the storefront the published ones for this store. Rendered by admin/view/template/catalog/bundle_list.twig, catalog/view/template/product/bundles.twig, catalog/view/template/product/bundle.twig, catalog/view/template/module/product_bundles.twig.href_all — a link to the Bundles page, built by core's url->link with the storefront's config_language. Rendered by catalog/view/template/module/product_bundles.twig.results — a row count, into a language string. Rendered by admin/view/template/catalog/bundle_list.twig, admin/view/template/sale/component_list.twig, catalog/view/template/product/bundles.twig.order_statuses — database — core's oc_order_status, for the report's status filter. Rendered by admin/view/template/sale/component.twig.filter_order_status_id — request — get[filter_order_status_id], cast to a non-negative integer by ComponentReport::filter(). Rendered by admin/view/template/sale/component.twig.filter_date_start — request — get[filter_date_start], narrowed to a real Y-m-d date or '' by ComponentReport::filter(). Rendered by admin/view/template/sale/component.twig.filter_date_end — request — get[filter_date_end], narrowed the same way. Rendered by admin/view/template/sale/component.twig.sort — request — get[sort], narrowed to the model's own sort whitelist. Rendered by admin/view/template/catalog/bundle_list.twig.order — request — get[order], narrowed to ASC or DESC. Rendered by admin/view/template/catalog/bundle_list.twig.product_bundles_bundle_id — database — the bundle's own primary key, or 0 on a new form. Rendered by admin/view/template/catalog/bundle_form.twig.model — database — the bundle's own model string. Rendered by admin/view/template/catalog/bundle_form.twig.price — database — the bundle's own price, formatted for the storefront in the visitor's currency. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig.price_mode — database — the bundle's own pricing mode. Rendered by admin/view/template/catalog/bundle_form.twig.discount — database — the bundle's own discount, formatted for the storefront. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig.discount_type — database — the bundle's own discount type. Rendered by admin/view/template/catalog/bundle_form.twig.status — database — the bundle's own status. Rendered by admin/view/template/catalog/bundle_form.twig.date_available — database — the bundle's own availability date. Rendered by admin/view/template/catalog/bundle_form.twig.sort_order — database — the bundle's own sort order. Rendered by admin/view/template/catalog/bundle_form.twig.weight — database — the bundle's own weight. Rendered by admin/view/template/catalog/bundle_form.twig.weight_class_id — database — the bundle's own weight class. Rendered by admin/view/template/catalog/bundle_form.twig.weight_classes — database — core's oc_weight_class_description. Rendered by admin/view/template/catalog/bundle_form.twig.length — database — the bundle's own dimension. Rendered by admin/view/template/catalog/bundle_form.twig.width — database — the bundle's own dimension. Rendered by admin/view/template/catalog/bundle_form.twig.height — database — the bundle's own dimension. Rendered by admin/view/template/catalog/bundle_form.twig.length_class_id — database — the bundle's own length class. Rendered by admin/view/template/catalog/bundle_form.twig.length_classes — database — core's oc_length_class_description. Rendered by admin/view/template/catalog/bundle_form.twig.tax_class_id — database — the bundle's own tax class. Rendered by admin/view/template/catalog/bundle_form.twig.tax_classes — database — core's oc_tax_class. Rendered by admin/view/template/catalog/bundle_form.twig.points — database — the bundle's own reward points. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig.reward — database — what this visitor's customer group earns for the bundle. Rendered by catalog/view/template/product/bundle.twig.bundle_reward — database — the reward points rows, one per customer group. Rendered by admin/view/template/catalog/bundle_form.twig.customer_groups — database — core's oc_customer_group_description. Rendered by admin/view/template/catalog/bundle_form.twig.currency_symbol_left — setting — the store's default currency. Rendered by admin/view/template/catalog/bundle_form.twig.currency_symbol_right — setting — the store's default currency. Rendered by admin/view/template/catalog/bundle_form.twig.currency_decimal_place — setting — the store's default currency. Rendered by admin/view/template/catalog/bundle_form.twig.languages — database — core's oc_language, unfiltered. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_description — database — the bundle's name, description and meta text, per language. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_seo_url — database — core's oc_seo_url rows keyed on this bundle, per store and language. Rendered by admin/view/template/catalog/bundle_form.twig.stores — database — core's oc_store, with store 0 named from the store's own config_name. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_store — database — which stores publish this bundle. Rendered by admin/view/template/catalog/bundle_form.twig.manufacturer_id — database — the bundle's own manufacturer. Rendered by admin/view/template/catalog/bundle_form.twig.manufacturer — database — that manufacturer's name out of core's oc_manufacturer. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_categories — database — core's category paths for the categories this bundle is filed under. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_filters — database — core's filter groups and filters this bundle carries. Rendered by admin/view/template/catalog/bundle_form.twig.bundle_components — database — the bundle's component rows, each named from core's oc_product_description. Rendered by admin/view/template/catalog/bundle_form.twig.image — database — the bundle's own image path. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig.thumb — a resized copy of that image, through core's tool/image model. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig, catalog/view/template/product/bundle_thumb.twig, catalog/view/template/product/bundles.twig.faults — database — what Health found wrong with this bundle's components, as language strings naming the products. Rendered by admin/view/template/catalog/bundle_form.twig.report — database — where this bundle is used and by whom. Rendered by admin/view/template/catalog/bundle_form.twig.name — database — the bundle's own name in the admin's language, or the storefront visitor's. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig, catalog/view/template/product/bundle_thumb.twig.description — database — the bundle's own description, rendered as the merchant's HTML. Rendered by catalog/view/template/product/bundle.twig, catalog/view/template/product/bundle_thumb.twig.bundle_id — database — the bundle the storefront page resolved to. Rendered by catalog/view/template/product/bundle.twig.product_id — database — the carrier product this bundle adds to the cart as. Rendered by admin/view/template/catalog/bundle_form.twig, catalog/view/template/product/bundle.twig.components — database — the component products, their options and their option values; on the components-sold report, each component product's catalogue name and model (or the copies frozen on the order) beside two counts. Rendered by catalog/view/template/product/bundle.twig, admin/view/template/catalog/bundle_list.twig, admin/view/template/sale/component_list.twig.configurable — database — whether any component has an option to choose. Rendered by catalog/view/template/product/bundle.twig.stock — database — what the least available component leaves the bundle able to promise. Rendered by catalog/view/template/product/bundle.twig.config_file_max_size — setting — the store's own upload ceiling, handed to core's tool/upload. Rendered by catalog/view/template/product/bundle.twig.language — setting — the storefront's config_language. Rendered by catalog/view/template/product/bundle.twig, admin/view/template/catalog/bundle_form.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/product_bundles.twig.Unverified beyond it: the other 648 of 697 template expressions in 16 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 — 49 url attributes carrying a template expression, of 49 sink sites asserted:extensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:45 — href="#language-{{ language.language_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 — 16 .twig files:extensions/product_bundles/src/admin/view/template/catalog/bundle.twig:61 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle.twig:63 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:403 — {{ bundle_components|length }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:408 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:488 — {{ currency_decimal_place }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:503 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:532 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:565 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/customer/purge.twig:71 — {{ remove }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/sale/component.twig:95 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/admin/view/template/sale/component.twig:99 — {{ user_token }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:247 — {{ price_route }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:247 — {{ language }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:249 — {{ payload_id_key }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:249 — {{ bundle_id }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:314 — {{ language }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:333 — {{ language }} is interpolated inside a <script> elementextensions/product_bundles/src/catalog/view/template/product/bundle.twig:354 — {{ language }} is interpolated inside a <script> element |
1.2.4 |
Every way this extension builds a database statement is written down beside the code, so how a value reaches a query is a published answer rather than something to go looking for. | declared — 3 ways of building a statement, over 98 statements run and 34 values escaped:escaped literal: every value this extension puts in a statement — Through $this->db->escape(), or an (int) cast where the value is numeric.interpolated identifier: the table name in the installer's SHOW TABLES / SHOW COLUMNS / SHOW INDEX probes, in Schema's CREATE TABLE statements, and in the order-delete cascade's two DELETEs — DB_PREFIX concatenated with a table name out of Schema's own constant list, at admin/model/module/product_bundles.php:119-160, system/library/schema.php:243, admin/model/sale/bundle.php and catalog/model/checkout/bundle.php (both deleteOrder(), looping Schema::orderTables()). Nothing from the request reaches it.whole-clause concatenation: the bundle list's ORDER BY and LIMIT — getBundles() concatenates the sort column only after in_array() has narrowed it to five literals it names itself (admin/model/catalog/bundle.php:232-234); the direction is one of two literals and the window is two integers. The components-sold report's getSold() (admin/model/sale/bundle.php) orders by a fixed clause and pages by two integer casts.10 of the 98 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 — 88 .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. | not met — 16 .twig files:extensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:61 — ckeditor binds a rich-text editor, and nothing in this extension sanitises the HTML it collectsextensions/product_bundles/src/admin/view/template/catalog/bundle_form.twig:601 — ckeditor binds a rich-text editor, and nothing in this extension sanitises the HTML it collects |
1.3.2 |
Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided. |
checked — 88 .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 — 88 .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 — 15 of 30 routes set a Content-Type of their own: 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="bundle-components- 7 × application/json 1 × application/json, with no charset (customer/personal_data.php:230) 1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299) 1 × application/json, with no charset (customer/personal_data.php:359) 1 × application/json, with no charset (customer/purge.php:168) 3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 1 × none — index() returns its markup to the position that asked and writes no output, so a direct request renders an empty page 1 × none — it registers a cart decorator and writes no response 13 × none — nothing sets a Content-Type, so the store's default stands |
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 — 20 call sites in 4 templates, each declared with what it writes there: 5 × .append(6 × .html(9 × .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 — 88 .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 — 88 .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 — 9 routes of 30 reaches a model write; the 7 admin ones among them stand behind the user_token core checks before dispatch, and 21 admin routes are gated that way in all:extensions/product_bundles/src/catalog/controller/product/bundle.php:91 — extension/product_bundles/product/bundle reaches a model write and stands behind the storefront session cookie alone; OpenCart carries no anti-CSRF token on the catalog side for it to check.extensions/product_bundles/src/catalog/controller/startup/cart.php:77 — extension/product_bundles/startup/cart reaches a model write and stands behind the storefront session cookie alone; OpenCart carries no anti-CSRF token on the catalog side for it to check. |
3.5.2 |
No route grants a cross-origin caller anything, so nothing here is left depending on a browser's preflight to refuse one. | checked — 88 .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 — 88 .php files:extensions/product_bundles/src/admin/controller/catalog/bundle.php:638 — Bundle::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/src/admin/controller/catalog/bundle.php:830 — Bundle::delete() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/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/product_bundles/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/product_bundles/src/admin/controller/module/product_bundles.php:486 — ProductBundles::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/src/admin/controller/module/product_bundles.php:539 — ProductBundles::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/src/admin/controller/module/product_bundles.php:693 — ProductBundles::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/src/catalog/controller/product/bundle.php:91 — Bundle::index() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/product_bundles/src/catalog/controller/startup/cart.php:77 — Cart::index() 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 — 15 of 30 routes set a Content-Type of their own: 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="bundle-components- 7 × application/json 1 × application/json, with no charset (customer/personal_data.php:230) 1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299) 1 × application/json, with no charset (customer/personal_data.php:359) 1 × application/json, with no charset (customer/purge.php:168) 3 × application/json; charset=utf-8, with X-Content-Type-Options: nosniff and Access-Control-Allow-Origin: null 1 × none — index() returns its markup to the position that asked and writes no output, so a direct request renders an empty page 1 × none — it registers a cart decorator and writes no response 13 × none — nothing sets a Content-Type, so the store's default stands |
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 30 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 30 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 — 16 .twig files |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | checked — 16 .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 — 88 .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 — 30 routes: 21 admin, 9 catalog, each declared beside the code |
8.2.1 |
An admin route that changes something tests the permission itself, in a condition that can refuse — and a route that only reads says so, standing behind the check OpenCart makes before dispatch. | declared — 21 admin routes: 10 pin a permission themselves, 0 at one same-class hop, 0 at two (the hop ceiling), 11 unpinned:extensions/product_bundles/src/admin/controller/catalog/bundle.php:38 — Bundle::index() pins no permission of its own; core checks access on extension/product_bundles/catalog/bundle before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/catalog/bundle.php:90 — Bundle::list() pins no permission of its own; core checks access on extension/product_bundles/catalog/bundle before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/catalog/bundle.php:101 — Bundle::getList() pins no permission of its own; core checks access on extension/product_bundles/catalog/bundle before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/catalog/bundle.php:229 — Bundle::form() pins no permission of its own; core checks access on extension/product_bundles/catalog/bundle before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/catalog/bundle.php:868 — Bundle::autocomplete() pins no permission of its own; core checks access on extension/product_bundles/catalog/bundle before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/product_bundles/customer/purge before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/event/catalog.php:301 — Catalog::refuse() pins no permission of its own; core checks access on extension/product_bundles/event/catalog before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/module/product_bundles.php:371 — ProductBundles::index() pins no permission of its own; core checks access on extension/product_bundles/module/product_bundles before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/sale/component.php:34 — Component::index() pins no permission of its own; core checks access on extension/product_bundles/sale/component before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/sale/component.php:79 — Component::list() pins no permission of its own; core checks access on extension/product_bundles/sale/component before dispatch. No model write is reachable from it.extensions/product_bundles/src/admin/controller/sale/component.php:95 — Component::export() pins no permission of its own; core checks access on extension/product_bundles/sale/component before dispatch. No model write is reachable from it. |
8.2.2 |
A storefront route that reaches a record says which caller may reach which records, and what selects one — so reaching somebody else's is a question with a written answer. | declared — 13 triples over 9 of 9 catalog routes; the admin half is one line on the shared page:extension/product_bundles/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/product_bundles/api/v1/bundle — a holder of one of core's oc_api credentials: The credential cannot be scoped. A bundle is the merchant's catalogue — its carrier, its stores and its components — and nothing on it is about a person. Selected by bundle_idextension/product_bundles/api/v1/bundle — a holder of one of core's oc_api credentials, walking the collection: A filter is a convenience for a caller who wants a narrower read, not a boundary: the same credential may simply leave it off. Selected by none — not a record; the filters and the cursor are a page of a walk rather than a record selectorextension/product_bundles/api/v1/line — a holder of one of core's oc_api credentials: The credential cannot be scoped. A sold line carries products, quantities and the options chosen on them, which may include text a shopper typed — an engraving — beside an order id; the customer's name and address are core's, on core's order, and are not read. Selected by opencart_order_product_idextension/product_bundles/api/v1/line — a holder of one of core's oc_api credentials, walking the collection: A filter is a convenience for a caller who wants a narrower read, not a boundary: the same credential may simply leave it off. Selected by none — not a record; the filters and the cursor are a page of a walk rather than a record selectorextension/product_bundles/event/cart.refuse — any visitor, signed in or not: It answers a language string and a link to the bundle page, and touches no row. Selected by none — not a record; $bundle_id only names the page the refusal links back toextension/product_bundles/product/bundle — any visitor, signed in or not: A visitor sees a bundle their storefront publishes; one assigned to another store, or switched off, is not found. Selected by get[product_bundles_bundle_id]extension/product_bundles/product/bundle — the visitor's customer group, signed in or not: The price quoted is the one the visitor's own group is entitled to. Selected by none — not a record; the group is taken from the session and core's config, never from the requestextension/product_bundles/product/bundle.price — any visitor, signed in or not: It re-quotes only a bundle this storefront publishes. Selected by post[product_bundles_bundle_id]extension/product_bundles/product/bundle.price — any visitor, signed in or not: A choice that is not one of that bundle's own option values is dropped rather than priced. Selected by none — not a record; post[bundle] is the shopper's own option selection, narrowed to the bundle's own option valuesextension/product_bundles/module/product_bundles — any visitor, signed in or not: Any visitor; it reads only enabled bundles assigned to this store, and none with a component this store cannot sell. Selected by get[product_id] on a product page, get[product_bundles_bundle_id] on a bundle pageextension/product_bundles/product/bundles — any visitor, signed in or not: The listing shows the bundles this storefront publishes, a page at a time. Selected by none — not a record; get[page] is a page numberextension/product_bundles/startup/cart — the visitor's own cart: It decorates the cart the request already has and addresses nobody else's. Selected by none — not a record; it reads no request key at all |
8.3.1 |
What bounds a caller to their own records comes from the server — a session, a stored row, the store id — and never from a value the caller supplied. | declared — 11 distinct bounds, each named by the triple it scopes: bounded by nothing — an OpenCart API user opens every store in the installation, and this resource does not narrow that bounded by core's cart, which is keyed on the session or the customer bounded by nothing is read from the database and nothing is written bounded by nothing — a filter is the caller narrowing their own read, never the store narrowing what they may see bounded by nothing — the refusal is the same for every caller bounded by product_bundles_bundle_to_store for the current config_store_id bounded by product_bundles_bundle_to_store for the current config_store_id, and the bundle's own status and date_available bounded by product_bundles_bundle_to_store for the current config_store_id, the bundle's own status and date_available, and packable(): every component a product this store sells, enabled and available bounded by the component products of the bundle the first selector named bounded by the customer-group discount rows the store set for the bundle bounded by the same store, status and date_available fence the page uses |
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 — 88 .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 — 88 .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 — 88 .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 — 88 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | checked — 88 .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 — 88 .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 — 16 .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 — 88 .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 — 66 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:module_product_bundles_status — settingmodule_product_bundles_module_limit — setting, held to MODULE_LIMIT_MIN..MODULE_LIMIT_MAX by Configuration::moduleLimit()version_warning — the store's OpenCart release, through Compatibilitylanguage_readout — rendered partial over oc_languageresolution — rendered from oc_language, through LanguageReadoutuser_token — the admin session token, rendered into this screen's own JavaScript URLsfilter_name — request — get[filter_name]bundles — database — product_bundles_bundle rows with their descriptions, and on the storefront the published ones for this storehref_all — a link to the Bundles page, built by core's url->link with the storefront's config_languageresults — a row count, into a language stringorder_statuses — database — core's oc_order_status, for the report's status filterfilter_order_status_id — request — get[filter_order_status_id], cast to a non-negative integer by ComponentReport::filter()filter_date_start — request — get[filter_date_start], narrowed to a real Y-m-d date or '' by ComponentReport::filter()filter_date_end — request — get[filter_date_end], narrowed the same waysort — request — get[sort], narrowed to the model's own sort whitelistorder — request — get[order], narrowed to ASC or DESCproduct_bundles_bundle_id — database — the bundle's own primary key, or 0 on a new formmodel — database — the bundle's own model stringprice — database — the bundle's own price, formatted for the storefront in the visitor's currencyprice_mode — database — the bundle's own pricing modediscount — database — the bundle's own discount, formatted for the storefrontdiscount_type — database — the bundle's own discount typestatus — database — the bundle's own statusdate_available — database — the bundle's own availability datesort_order — database — the bundle's own sort orderweight — database — the bundle's own weightweight_class_id — database — the bundle's own weight classweight_classes — database — core's oc_weight_class_descriptionlength — database — the bundle's own dimensionwidth — database — the bundle's own dimensionheight — database — the bundle's own dimensionlength_class_id — database — the bundle's own length classlength_classes — database — core's oc_length_class_descriptiontax_class_id — database — the bundle's own tax classtax_classes — database — core's oc_tax_classpoints — database — the bundle's own reward pointsreward — database — what this visitor's customer group earns for the bundlebundle_reward — database — the reward points rows, one per customer groupcustomer_groups — database — core's oc_customer_group_descriptioncurrency_symbol_left — setting — the store's default currencycurrency_symbol_right — setting — the store's default currencycurrency_decimal_place — setting — the store's default currencylanguages — database — core's oc_language, unfilteredbundle_description — database — the bundle's name, description and meta text, per languagebundle_seo_url — database — core's oc_seo_url rows keyed on this bundle, per store and languagestores — database — core's oc_store, with store 0 named from the store's own config_namebundle_store — database — which stores publish this bundlemanufacturer_id — database — the bundle's own manufacturermanufacturer — database — that manufacturer's name out of core's oc_manufacturerbundle_categories — database — core's category paths for the categories this bundle is filed underbundle_filters — database — core's filter groups and filters this bundle carriesbundle_components — database — the bundle's component rows, each named from core's oc_product_descriptionimage — database — the bundle's own image paththumb — a resized copy of that image, through core's tool/image modelfaults — database — what Health found wrong with this bundle's components, as language strings naming the productsreport — database — where this bundle is used and by whomname — database — the bundle's own name in the admin's language, or the storefront visitor'sdescription — database — the bundle's own description, rendered as the merchant's HTMLbundle_id — database — the bundle the storefront page resolved toproduct_id — database — the carrier product this bundle adds to the cart ascomponents — database — the component products, their options and their option values; on the components-sold report, each component product's catalogue name and model (or the copies frozen on the order) beside two countsconfigurable — database — whether any component has an option to choosestock — database — what the least available component leaves the bundle able to promiseconfig_file_max_size — setting — the store's own upload ceiling, handed to core's tool/uploadlanguage — setting — the storefront's config_languageapi_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 — 88 .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 — 88 .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 — 88 .php files |