Security verdict¶
21 checked, 3 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 — 2 bearer-secret surfaces over 3 mint, compare and refuse sites (2 × mint, 1 × constant-time compare, 0 × compare, 0 × refusal); 1 further entry says what the derivation reached that is not a secret: the API credential, presented as Authorization: Basic <base64 username:key> and compared at system/library/api_gateway.php:948; parsed at :906 and looked up against core's oc_api table by catalog/controller/event/api.php. 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.core's user_token, carried as a link fragment by every admin screen this extension builds — admin/controller/module/b2b_pricing.php:207 is where the fragment is assembled. Minted and checked by core's own admin startup. No comparison of it happens in this extension's own code. No refusal of an unset value sits in this extension's own code. Minted by core, so no entropy is asserted here: nothing in this baseline rests on core's own token helper. |
1.2.1 |
Store data meets markup safely where the danger is decidable — an unquoted attribute, a URL the template composed itself, a style, hand-built XML — and every store-derived subtree a template of this extension renders is written down beside the code. Beyond those two, nothing is claimed, and the page says so. |
declared — machine-pass on the sinks: 0 sink sites asserted here, 0 not admitted; attested over the inventory: 1 store-derived subtree over 1 template; unverified beyond it: everything else:module_b2b_pricing_status — the module_b2b_pricing setting. Rendered by admin/view/template/module/b2b_pricing.twig.Unverified beyond it: the other 561 of 611 template expressions in 17 templates, and any $data subtree nobody enumerated. The inventory is an inventory and not a bound: completeness over the whole expression surface is unverifiable, so this residual is permanent, and it is published rather than left to be inferred from what is missing. |
1.2.2 |
A URL a template builds for itself, rather than taking one whole from the link helper, has every value in it URL-encoded — so nothing a store holds can add a parameter of its own or change where the link goes. | declared — 50 url attributes carrying a template expression, of 50 sink sites asserted: Every url attribute in this extension's templates takes its value whole from the link helper. |
1.2.3 |
No template expression is interpolated into a <script> element, so store data cannot end a string literal and start running. |
not met — 17 .twig files:extensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:165 — {{ target_price }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:166 — {{ target_case }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:171 — {{ count }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:182 — {{ text_nothing_yet }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:182 — {{ text_none }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/bulk.twig:197 — {{ apply }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/case_quantity.twig:113 — {{ text_confirm }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_check.twig:168 — {{ autocomplete }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_check.twig:189 — {{ autocomplete }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_check.twig:224 — {{ user_token }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list.twig:78 — {{ user_token }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list.twig:80 — {{ user_token }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_assignment.twig:80 — {{ autocomplete }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_form.twig:149 — {{ text_confirm }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_form.twig:174 — {{ b2b_pricing_list_id }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_form.twig:199 — {{ b2b_pricing_list_id }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_form.twig:225 — {{ text_confirm }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_form.twig:259 — {{ b2b_pricing_list_id }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_import.twig:107 — {{ upload }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_row.twig:100 — {{ autocomplete }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_row.twig:119 — {{ b2b_pricing_list_id }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/catalog/price_list_row.twig:127 — {{ user_token }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/b2b_pricing/src/admin/view/template/customer/purge.twig:71 — {{ remove }} 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 94 statements run and 17 values escaped:a statement handed over already built — the COUNT(*) PurgeCounts::total() runs is composed by its caller and arrives as a string (admin/model/customer/purge_counts.php). Nothing composes one to hand it yet; the first counts() that does will interpolate a table name the declaration owns between backticks, and that mechanism belongs here beside this onea statement handed over already built — the UPDATE that clears this extension's own oc_cart.override rows is composed by Override::sweep() (system/library/override.php) and run by three callers with three scopes — the storefront pass standing down, uninstall(), and the erasure. The marker it matches on is a constant of this extension's; the scope arrives already cast, built in the model beside core's own statements. It is composed rather than parameterised for the reason Schema's statements are: one spelling of the predicate is what makes a sweep that misses nothingcore-style concatenation in the admin model layer — every statement admin/model/catalog/price_list.php runs is built by concatenation, with (int) on each number and $this->db->escape() on each string — core's own idiom, kept because this model sits beside core's and a second idiom in the same layer is a second thing a reviewer has to learn. The two sort keys a query string can reach are narrowed against a whitelist before they meet the ORDER BY rather than escaped into it, because a column name is not a value and escaping is the wrong tool for it14 of the 94 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 — 80 .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 — 17 .twig files |
1.3.2 |
Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided. |
checked — 80 .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 — 80 .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 — 32 of 53 routes set a Content-Type of their own: 4 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="b2b-pricing-list- 11 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...). Reads the raw $_GET and the raw $_SERVER rather than core's cleaned copies, because Request::clean() runs trim(htmlspecialchars()) over every superglobal for Twig's benefit14 × application/json 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) 3 × none set, and no output written 4 × none set; the page goes out under whatever the front controller defaults to 1 × none — it writes a column on the shopper's own cart rows and no response at all 13 × none — nothing sets a Content-Type, so the store's default stands 1 × set by ApiAnswer::headers($_SERVER, ...), whose list carries Content-Type: application/json; charset=utf-8. This route reads the raw $_SERVER rather than $this->request->server |
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 — 18 call sites in 6 templates, each declared with what it writes there: 2 × .append(16 × .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 — 80 .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 — 80 .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 — 21 routes of 53 reaches a model write; the 16 admin ones among them stand behind the user_token core checks before dispatch, and 39 admin routes are gated that way in all:extensions/b2b_pricing/src/catalog/controller/api/v1/assignment.php:97 — extension/b2b_pricing/api/v1/assignment.create 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/b2b_pricing/src/catalog/controller/api/v1/assignment.php:191 — extension/b2b_pricing/api/v1/assignment.delete 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/b2b_pricing/src/catalog/controller/api/v1/price_list.php:79 — extension/b2b_pricing/api/v1/price_list.create 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/b2b_pricing/src/catalog/controller/api/v1/price_list.php:102 — extension/b2b_pricing/api/v1/price_list.delete 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/b2b_pricing/src/catalog/controller/startup/cart.php:67 — extension/b2b_pricing/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 — 80 .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 — 80 .php files:extensions/b2b_pricing/src/admin/controller/catalog/bulk.php:198 — Bulk::apply() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/case_quantity.php:213 — CaseQuantity::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/case_quantity.php:268 — CaseQuantity::delete() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:286 — PriceList::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:343 — PriceList::delete() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:379 — PriceList::copy() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:544 — PriceList::saveRow() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:605 — PriceList::deleteRow() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:716 — PriceList::upload() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:992 — PriceList::saveAssignment() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/catalog/price_list.php:1070 — PriceList::deleteAssignment() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/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/b2b_pricing/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/b2b_pricing/src/admin/controller/module/b2b_pricing.php:257 — B2bPricing::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/module/b2b_pricing.php:303 — B2bPricing::install() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/admin/controller/module/b2b_pricing.php:516 — B2bPricing::uninstall() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/catalog/controller/api/v1/assignment.php:97 — Assignment::create() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/catalog/controller/api/v1/assignment.php:191 — Assignment::delete() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/catalog/controller/api/v1/price_list.php:79 — PriceList::create() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/catalog/controller/api/v1/price_list.php:102 — PriceList::delete() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/b2b_pricing/src/catalog/controller/startup/cart.php:67 — 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 — 32 of 53 routes set a Content-Type of their own: 4 × Content-Type: application/json, with no charset 1 × Content-Type: text/csv; charset=utf-8, beside Content-Disposition: attachment; filename="b2b-pricing-list- 11 × application/json; charset=utf-8, from ApiAnswer::headers($_SERVER, ...). Reads the raw $_GET and the raw $_SERVER rather than core's cleaned copies, because Request::clean() runs trim(htmlspecialchars()) over every superglobal for Twig's benefit14 × application/json 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) 3 × none set, and no output written 4 × none set; the page goes out under whatever the front controller defaults to 1 × none — it writes a column on the shopper's own cart rows and no response at all 13 × none — nothing sets a Content-Type, so the store's default stands 1 × set by ApiAnswer::headers($_SERVER, ...), whose list carries Content-Type: application/json; charset=utf-8. This route reads the raw $_SERVER rather than $this->request->server |
5.2.1 |
An upload is accepted on the server's terms — what the bytes are, not what the caller said they were — and every surface that takes one is declared. | declared — 1 upload surface across 53 routes:extension/b2b_pricing/catalog/price_list.upload — $this->request->files['file'], a CSV of prices. The temporary name is put through is_uploaded_file() and read where it lies; nothing is moved, no name from the upload reaches a path, and the refused lines go back in the JSON reply rather than to a file anybody has to clean up |
5.2.2 |
An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands. | declared — 1 upload surface across 53 routes:extension/b2b_pricing/catalog/price_list.upload — $this->request->files['file'], a CSV of prices. The temporary name is put through is_uploaded_file() and read where it lies; nothing is moved, no name from the upload reaches a path, and the refused lines go back in the JSON reply rather than to a file anybody has to clean up |
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 — 3 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 — 3 write sites, each declared with its file:line and pinned against the token stream both ways:3 × log file |
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 — 17 .twig files |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | checked — 17 .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 — 80 .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 — 53 routes: 39 admin, 14 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 — 39 admin routes: 19 pin a permission themselves, 0 at one same-class hop, 1 at two (the hop ceiling), 19 unpinned:extensions/b2b_pricing/src/admin/controller/catalog/bulk.php:86 — Bulk::index() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/bulk before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/bulk.php:162 — Bulk::count() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/bulk before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/case_quantity.php:46 — CaseQuantity::index() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/case_quantity before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/case_quantity.php:99 — CaseQuantity::list() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/case_quantity before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/case_quantity.php:112 — CaseQuantity::getList() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/case_quantity before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_check.php:117 — PriceCheck::autocomplete() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_check before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:49 — PriceList::index() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:101 — PriceList::list() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:114 — PriceList::getList() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:211 — PriceList::form() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:413 — PriceList::row() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:424 — PriceList::getRowList() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:645 — PriceList::import() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:880 — PriceList::export() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:917 — PriceList::assignment() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:928 — PriceList::getAssignmentList() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/catalog/price_list.php:1100 — PriceList::autocomplete() pins no permission of its own; core checks access on extension/b2b_pricing/catalog/price_list before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/b2b_pricing/customer/purge before dispatch. No model write is reachable from it.extensions/b2b_pricing/src/admin/controller/module/b2b_pricing.php:210 — B2bPricing::index() pins no permission of its own; core checks access on extension/b2b_pricing/module/b2b_pricing 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 13 of 14 catalog routes; the admin half is one line on the shared page:extension/b2b_pricing/startup/cart — the visitor's own cart: It prices the cart the request already has and addresses nobody else's. Selected by none — not a record; it reads no request key at allextension/b2b_pricing/api/gateway.fail — any caller, credentialled or not: A refusal envelope reads nothing and partitions nothing; it exists so that a convention failure cannot answer with a different header list from a success. Selected by none — not a recordextension/b2b_pricing/api/v1/price_list — the merchant's own integrator, holding an oc_api credential: The API is a merchant-to-merchant surface: there is no second API identity to partition against, and the convention has no per-resource scoping for one to be expressed with. Selected by list_id (get), or a filtered page where it is absentextension/b2b_pricing/api/v1/price_list.create — the merchant's own integrator, holding an oc_api credential: A new list is born switched off with no rungs and nobody on it, so the call changes no price anybody is charged. Selected by none — it creates a list and addresses noneextension/b2b_pricing/api/v1/price_list.delete — the merchant's own integrator, holding an oc_api credential: Refused with 409 list_assigned while a live assignment points at the list, which is a domain refusal and not an authorisation one. Selected by list_id (get) — the only parameter a transition acceptsextension/b2b_pricing/api/v1/price_list.empty — the merchant's own integrator, holding an oc_api credential: Refused with 409 list_assigned on the same predicate as delete: emptying a live list un-prices every customer on it in one call. Selected by list_id (get) — the only parameter a transition acceptsextension/b2b_pricing/api/v1/row — the merchant's own integrator, holding an oc_api credential: The API is a merchant-to-merchant surface: there is no second API identity to partition against, and the convention has no per-resource scoping for one to be expressed with. Selected by filter_list_id (get), required — the collection has no id of its ownextension/b2b_pricing/api/v1/row.upsert — the merchant's own integrator, holding an oc_api credential: The one write here allowed against a list somebody is on: a rung is independently valid, and an unmatched identifier is reported rather than refused. Selected by none in the query string — the list is named in the body, so a mismatch between the two is not expressibleextension/b2b_pricing/api/v1/assignment — the merchant's own integrator, holding an oc_api credential: The API is a merchant-to-merchant surface: there is no second API identity to partition against, and the convention has no per-resource scoping for one to be expressed with. Selected by none — a filtered page, which is the whole read surfaceextension/b2b_pricing/api/v1/assignment.create — the merchant's own integrator, holding an oc_api credential: Exactly one of customer_id and customer_group_id, refused in the method; the pair is an invariant no CREATE TABLE can state. Selected by none — everything it takes is in the bodyextension/b2b_pricing/api/v1/assignment.move — the merchant's own integrator, holding an oc_api credential: The one call that changes what a buyer is charged. Who the assignment is for is not among the columns it writes. Selected by assignment_id (get)extension/b2b_pricing/api/v1/assignment.delete — the merchant's own integrator, holding an oc_api credential: The list and its rungs are untouched; what changes is that nobody is priced by it any more. Selected by assignment_id (get)extension/b2b_pricing/api/v1/price — the merchant's own integrator, holding an oc_api credential: It reads what one named customer pays, which is no new leak: a caller who can read lists and assignments can already derive it. The credential is the merchant's own. Selected by product_id with customer_id or customer_group_id (get) |
8.3.1 |
What bounds a caller to their own records comes from the server — a session, a stored row, the store id — and never from a value the caller supplied. | declared — 3 distinct bounds, each named by the triple it scopes: bounded by nothing — no record is read bounded by nothing — the credential is the merchant's own and every price list in the installation is theirs to read; the filters narrow the answer rather than bounding it bounded by store, customer and session together, on every statement, exactly as core's own Cart selects. A cart_id reaching the UPDATE came out of a row this session was handed, and a statement trusting it alone would be one a crafted id could aim at somebody else's cart |
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 2 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 — 80 .php files |
9.1.3 |
The key a signed secret is checked against comes from somewhere this extension was configured with, never from anything inside the secret itself. | declared — 2 bearer-secret surfaces, from core — never from anything inside the secret presented:system/library/api_gateway.php:948 — coreadmin/controller/module/b2b_pricing.php:207 — 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 2 bearer-secret surfaces could carry a validity span inside the secret itself; the rest are a reference to a row, whose expiry is a column on it rather than a claim the caller presents: 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 — 80 .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 — 80 .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 — 80 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | checked — 80 .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 — 80 .php files |
14.2.1 |
A credential is not carried in a URL, where a browser history, a referrer header and a proxy log each keep their own copy of it. | not met — 1 bearer-secret surface of 2 travels in a URL:admin/controller/module/b2b_pricing.php:207 — the query string of every admin link this extension builds |
14.3.1 |
Nothing is left behind in the browser's own storage for the next person at that computer to read. | checked — 17 .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 — 80 .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 — 1 store-derived subtree reaches a template of this extension, each one written down; what a model row holds beyond them does not:module_b2b_pricing_status — the module_b2b_pricing setting |
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 — 80 .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 — 80 .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 — 80 .php files |