Security verdict¶
17 checked, 7 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 4 mint, compare and refuse sites (0 × mint, 1 × constant-time compare, 0 × compare, 3 × refusal); 3 further entries say what the derivation reached that is not a secret: the feed secret, read at catalog/controller/feed/preflight.php:65 and compared with hash_equals() at system/library/feed_access.php:57 after Escaping::decode() has undone what Request::clean() did to it. An empty one refuses everybody rather than admitting them (feed_access.php:35). Compared in constant time at system/library/feed_access.php:57. Refused when unset at system/library/feed_access.php:35. No entropy floor is asserted here, and none could be: this secret is typed by the operator, the entropy of a human-chosen string is not computable, and reporting its length as entropy would make this page claim what it has not measured. Mint it instead of typing it and this sentence goes away.core's user_token, carried as a link fragment by every admin screen this extension builds — admin/controller/module/preflight.php:2872 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: 4 sink sites asserted here, 2 not admitted; attested over the inventory: 36 store-derived subtrees over 2 templates; unverified beyond it: everything else:extensions/preflight/src/admin/view/template/module/preflight.twig:58 — style="width: {% if progress.total %}{{ progress.percent }}{% else %}100{% endif %}%" puts a value in a CSS context, which nothing in Twig escapes forextensions/preflight/src/system/library/sheet_export.php:71 — <table:table-row> is hand-built XML taking a value with no escaper standing in front of itmapping — supplier and operator: the column names the uploaded file carried, against the fields the operator bound them to, with their transforms and arguments. Rendered by admin/view/template/module/preflight.twig.samples — supplier: the first rows of the uploaded file, printed verbatim so the operator can see what they are about to import. Rendered by admin/view/template/module/preflight.twig.ops — store and supplier: one row per planned change, carrying the value the catalog holds and the value the feed would write. Rendered by admin/view/template/module/preflight.twig.fields — store: the field names the plan touches, off the entity's own SHOW COLUMNS, with a record count each. Rendered by admin/view/template/module/preflight.twig.anomalies — store and supplier: the sentences naming what looks wrong about a plan, each quoting the figures it is about. Rendered by admin/view/template/module/preflight.twig.found — store: the records a search matched, each carrying its own record label out of the catalog. Rendered by admin/view/template/module/preflight.twig.needle — operator: the search term, echoed back into the form. Rendered by admin/view/template/module/preflight.twig.rollback — store: the journal of one job — what was restored, deleted or recreated, with the before and after of every field it touched. Rendered by admin/view/template/module/preflight.twig.summary — store: the seven counts a job ended with, read back off the job row. Rendered by admin/view/template/module/preflight.twig.progress — store: where a running job has got to, read back off the job row. Rendered by admin/view/template/module/preflight.twig.history — store: past jobs, each carrying the name of the file it read and the operator who started it. Rendered by admin/view/template/module/preflight.twig.drafts — store: mappings saved and not yet run, each under a name the operator typed. Rendered by admin/view/template/module/preflight.twig.profiles — operator: saved profiles, each under a name the operator typed. The same names reach the store log undecoded from the cron route — see the cron/preflight.schedule entry under routes. Rendered by admin/view/template/module/preflight.twig.runs — store and operator: what each scheduled run did, carrying the profile's name and, where it failed, the exception's own message. Rendered by admin/view/template/module/preflight.twig.estimate — store: how long a job of this size is likely to take on this store, measured from its own past runs. Rendered by admin/view/template/module/preflight.twig.error_warning — store: the sentence explaining a refusal, which for a PreflightException carries whatever the exception named — a directory, a column, a value out of the file. Rendered by admin/view/template/module/preflight.twig.success — store: the sentence confirming what was done, which names the profile or the job it was done to. Rendered by admin/view/template/module/preflight.twig.root — operator: the directory path the operator typed into the settings card, or the shipped default. Rendered by admin/view/template/module/preflight.twig.exports — operator: the export directory path the operator typed into the settings card. Rendered by admin/view/template/module/preflight.twig.entity — operator: the kind of record chosen on the screen, narrowed to Entities::has() before it is used. Rendered by admin/view/template/module/preflight.twig.entity_note — store: the sentence this screen prints about the chosen entity, which is a language string and empty where there is none. Rendered by admin/view/template/module/preflight.twig.key_field — store: the entity's own primary key column name. Rendered by admin/view/template/module/preflight.twig.match_fields — store: the columns of the chosen entity a row may be matched on. Rendered by admin/view/template/module/preflight.twig.export_groups — store: every field of the chosen entity, grouped, off the field catalog the database's own SHOW COLUMNS built. Rendered by admin/view/template/module/preflight.twig.export_sorts — store: the fields of the chosen entity an export may be ordered by. Rendered by admin/view/template/module/preflight.twig.export_filters — store: the fields of the chosen entity an export may be narrowed by. Rendered by admin/view/template/module/preflight.twig.export — store: the export profile being edited — its name, fields, format and filters as saved in preflight_profile — or, on a fresh form, the matchable columns of the chosen entity, preselected — only model and SKU for a product. Rendered by admin/view/template/module/preflight.twig.export_languages — store: the names of the languages the store has installed. Rendered by admin/view/template/module/preflight.twig.entities — store: the entity options, each with the label the store's own language file gives it. Rendered by admin/view/template/module/preflight.twig.feed_url — store: HTTP_CATALOG out of the store's own configuration, with the chosen entity appended. Rendered by admin/view/template/module/preflight.twig.ai_provider — operator: which provider the operator chose. The key itself is never sent to the screen — ai_key_set is a boolean saying whether one is stored, and nothing more. Rendered by admin/view/template/module/preflight.twig.ai_model — store: the model that provider is being asked for, derived from the chosen provider. Rendered by admin/view/template/module/preflight.twig.version_status — store: what Compatibility::status() makes of the store's own VERSION. Rendered by admin/view/template/module/preflight.twig.version_warning — store: the sentence naming the store's own OpenCart version where it is outside the tested range. Rendered by admin/view/template/module/preflight.twig.download — store: the link to the finished export of the job on screen, built from the job id. Rendered by admin/view/template/module/preflight.twig.resolution — store: the language readout's own payload — the names of the languages the store has installed and which of them this extension ships — handed to the second view file directly rather than through render(). Rendered by admin/view/template/module/language_readout.twig.Unverified beyond it: the other 710 of 772 template expressions in 6 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 — 58 url attributes carrying a template expression, of 62 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 — 6 .twig files:extensions/preflight/src/admin/view/template/customer/personal_data.twig:154 — {{ erase }} is interpolated inside a <script> elementextensions/preflight/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 — 4 ways of building a statement, over 72 statements run and 82 values escaped:escaped string literal — '" . $this->db->escape($value) . "' inside a quoted literal. This is the mechanism every value a supplier's file carried goes through on its way into the catalog — system/library/port/database_store.php:677, :733, :850 and the restinteger cast into a literal — '" . (int)$id . "' — record ids, job ids, offsets and limitsinterpolated identifier — table and column names interpolated between backticks: '' . $field . '' and 'INSERT INTO' . $table . '' throughout system/library/port/database_store.php, and DB_PREFIX . $table in the four schema-introspection statements at admin/model/module/preflight.php:120, :128, :153 and :161. This is the mechanism that makes the extension what it is — a field name is a value on this screen — and the names come from the field catalog the database's own SHOW COLUMNS built, from Entities and from Target, never from a request directly. A name the catalog does not carry is dropped before it reaches a statement, which is what FieldCatalog and Entities::key() are forwhole-clause concatenation — restrict(), member() and ordering() (system/library/port/database_store.php:503, :610, :638) build WHERE, IN (…) and ORDER BY fragments and interpolate the finished clause into the statement. The operator's own filter text is escaped inside the fragment (:528, :550, :554); the column names around it come from the catalog, and the direction is narrowed to the two literals ASC and DESC (:651)5 of the 72 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 — 159 .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 — 6 .twig files |
1.3.2 |
Nothing runs code it assembled while running — no eval(), and no include of a path a variable decided. |
checked — 159 .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 — 159 .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 — 8 of 39 routes set a Content-Type of their own: 1 × the one computed Content-Type sink in this repo: Content-Type: . mime($path) (admin/controller/module/preflight.php:1395), which maps the staged file's own extension through a six-entry table and falls back to application/octet-stream. One of those six is image/svg+xml with no charset (:1415). An SVG is a document, and one served from the admin's own origin could run script with the administrator's session, so the answer also carries X-Content-Type-Options: nosniff (:1389) and Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox (:1394)1 × application/octet-stream, with no charset (admin/controller/module/preflight.php:677), and X-Content-Type-Options: nosniff at :673. Sent through header() rather than the response, because Response::output() sends nothing at all — headers included — when the body is streamed rather than held1 × application/octet-stream, with no charset (catalog/controller/feed/preflight.php:136), and X-Content-Type-Options: nosniff at :140. Sent through header() rather than the response, because Response::output() sends nothing at all when the body is streamed; an export served without its Content-Type is served as text/html, and an export is full of text a supplier's feed supplied1 × application/json, with no charset (admin/controller/module/preflight.php:919) 1 × application/json, with no charset (customer/personal_data.php:230) 1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299) 1 × application/json, with no charset (customer/personal_data.php:359) 1 × application/json, with no charset (customer/purge.php:168) 1 × none set, and no output written 30 × 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 — 5 call sites in 2 templates, each declared with what it writes there: 2 × .append(3 × .prepend( |
3.3.1 |
A cookie this extension sets carries the Secure attribute at the call that sets it, so a browser cannot send it back over plain HTTP. |
checked — 159 .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 — 159 .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 — 4 routes of 39 reaches a model write; the 4 admin ones among them stand behind the user_token core checks before dispatch, and 34 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 — 159 .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 — 159 .php files:extensions/preflight/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/preflight/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/preflight/src/admin/controller/module/preflight.php:1103 — Preflight::save() writes through a model and never reads REQUEST_METHOD, so a GET anybody can cause does the same thing a POST doesextensions/preflight/src/admin/controller/module/preflight.php:1520 — Preflight::install() 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 — 8 of 39 routes set a Content-Type of their own: 1 × the one computed Content-Type sink in this repo: Content-Type: . mime($path) (admin/controller/module/preflight.php:1395), which maps the staged file's own extension through a six-entry table and falls back to application/octet-stream. One of those six is image/svg+xml with no charset (:1415). An SVG is a document, and one served from the admin's own origin could run script with the administrator's session, so the answer also carries X-Content-Type-Options: nosniff (:1389) and Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox (:1394)1 × application/octet-stream, with no charset (admin/controller/module/preflight.php:677), and X-Content-Type-Options: nosniff at :673. Sent through header() rather than the response, because Response::output() sends nothing at all — headers included — when the body is streamed rather than held1 × application/octet-stream, with no charset (catalog/controller/feed/preflight.php:136), and X-Content-Type-Options: nosniff at :140. Sent through header() rather than the response, because Response::output() sends nothing at all when the body is streamed; an export served without its Content-Type is served as text/html, and an export is full of text a supplier's feed supplied1 × application/json, with no charset (admin/controller/module/preflight.php:919) 1 × application/json, with no charset (customer/personal_data.php:230) 1 × application/json, with no charset (customer/personal_data.php:272), sent as an attachment named for the person and the day (customer/personal_data.php:299) 1 × application/json, with no charset (customer/personal_data.php:359) 1 × application/json, with no charset (customer/purge.php:168) 1 × none set, and no output written 30 × 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 — 2 upload surfaces across 39 routes:extension/preflight/module/preflight.plan — $this->request->files['source'] (admin/controller/module/preflight.php:376), the supplier's data file. is_uploaded_file() and UPLOAD_ERR_OK are both required at :369 before anything is staged. The format is decided from the file's own bytes by SourceFormat, never from the uploaded name or from any client-supplied type; the uploaded name's extension is carried onto the staged copy for a human looking in the upload directory and for nothing else. The staged name is uniqid('', true) under StoreFiles::STAGED, so nothing the caller named reaches the path. There is no byte ceiling of our own — what bounds the size is PHP's own upload_max_filesize and post_max_sizeextension/preflight/module/preflight.importProfile — $this->request->files['profile'] (admin/controller/module/preflight.php:945), a profile exported from this same screen. It is read as JSON and rebuilt field by field through Profile; nothing in it becomes a path, a table name or a column name without going through the field catalog first. The uploaded file is never moved out of PHP's own temporary directory |
5.2.2 |
An uploaded file is stored under a name the server chose, so nothing the caller named decides where it lands. | declared — 2 upload surfaces across 39 routes:extension/preflight/module/preflight.plan — $this->request->files['source'] (admin/controller/module/preflight.php:376), the supplier's data file. is_uploaded_file() and UPLOAD_ERR_OK are both required at :369 before anything is staged. The format is decided from the file's own bytes by SourceFormat, never from the uploaded name or from any client-supplied type; the uploaded name's extension is carried onto the staged copy for a human looking in the upload directory and for nothing else. The staged name is uniqid('', true) under StoreFiles::STAGED, so nothing the caller named reaches the path. There is no byte ceiling of our own — what bounds the size is PHP's own upload_max_filesize and post_max_sizeextension/preflight/module/preflight.importProfile — $this->request->files['profile'] (admin/controller/module/preflight.php:945), a profile exported from this same screen. It is read as JSON and rebuilt field by field through Profile; nothing in it becomes a path, a table name or a column name without going through the field catalog first. The uploaded file is never moved out of PHP's own temporary directory |
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 — 38 write sites, 8 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/port/store_files.php:269 — system/library/port/store_files.php:269 — the installed image, moved into DIR_IMAGE under the folder ImageSettings::FOLDER names. Inside the document root. The extension is decided from the bytes and never from the source's name or a server-supplied content type, and the rollback copy is taken before this line rather than aftersystem/library/port/store_files.php:269 — system/library/port/store_files.php:269 — the same install, where rename() could not do it across a filesystem boundary. Inside the document rootsystem/library/port/store_files.php:298 — system/library/port/store_files.php:298 — rolling an apply back: the image this job replaced, copied out of the journal and over the installed onesystem/library/port/store_files.php:305 — system/library/port/store_files.php:305 — rolling an apply back: an image this job added, where the journal records that nothing was there beforesystem/library/port/store_files.php:627 — system/library/port/store_files.php:627 — the directories all of the above live in, created recursively with StoreFiles::MODE, which is 0755: the folder under DIR_IMAGE is inside the document root, and world-write is not a bit a public directory is given |
5.3.2 |
Every path this extension writes to is written down beside the code, with where the name in it came from. | declared — 38 write sites, each declared with its file:line and pinned against the token stream both ways:3 × copy 5 × fopen 9 × fwrite 3 × log file 1 × mkdir 1 × move_uploaded_file 4 × rename 1 × rmdir 1 × touch 10 × unlink |
6.2.6 |
A field that takes a password or a key is masked, so it is not left readable on the screen or in a screenshot of it. | not met — 6 .twig files:extensions/preflight/src/admin/view/template/module/preflight.twig:1311 — module_preflight_feed_secret takes a secret and is not type="password", so what is typed into it stays readable on the screen |
6.2.7 |
A masked field does not refuse a paste or shut a password manager out of it. | not met — 6 .twig files:extensions/preflight/src/admin/view/template/module/preflight.twig:1339 — autocomplete="off" on a masked field shuts a browser password helper or a paste out of it |
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 — 159 .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 — 39 routes: 34 admin, 5 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 — 34 admin routes: 10 pin a permission themselves, 21 at one same-class hop, 0 at two (the hop ceiling), 3 unpinned:extensions/preflight/src/admin/controller/customer/purge.php:83 — Purge::index() pins no permission of its own; core checks access on extension/preflight/customer/purge before dispatch. No model write is reachable from it.extensions/preflight/src/admin/controller/module/preflight.php:272 — Preflight::index() pins no permission of its own; core checks access on extension/preflight/module/preflight before dispatch. No model write is reachable from it.extensions/preflight/src/admin/controller/module/preflight.php:336 — Preflight::kind() pins no permission of its own; core checks access on extension/preflight/module/preflight before dispatch. No model write is reachable from it. |
8.2.2 |
A storefront route that reaches a record says which caller may reach which records, and what selects one — so reaching somebody else's is a question with a written answer. | declared — 5 triples over 4 of 5 catalog routes; the admin half is one line on the shared page:extension/preflight/cli/preflight — whoever can run PHP on the server: The command line is the supported door for a store whose cron.php is dead, and it is deliberately not reachable over HTTP. Selected by none — not a record; the arguments come from the bootstrap's $argv and a request carries noneextension/preflight/cron/preflight — the store's own scheduler: This route logs at catalog/controller/cron/preflight.php:91, through the extension's own Diary; the job names it lists are the store owner's own, and the writer neutralises every message before it lands. Selected by none — not a record; the route reads no request key at allextension/preflight/cron/preflight.schedule — the store's own scheduler: This route writes operator-typed text into the diagnostic log, decoded. $profile->name and $run->name reach the extension's own Diary at :160, :182 and :291, and a profile name went through Escaping::decode() over the whole POST when it was saved while the matching encode is applied at exactly one place in this extension — the admin template boundary, which a cron controller is not. What the name can no longer do is forge a line: the writer neutralises every message through LogLine::safe() before it renders one, which is where that used to be the call site's job and where a known Low was declared. The text is still the operator's own, which is why log injection is asserted totally rather than bounded. Selected by none — not a record; the route reads no request key at allextension/preflight/feed/preflight — anybody holding the feed secret: There is one secret for the whole store and no per-partner narrowing: a partner given the URL for products can read every other fed entity by changing one parameter. Which entities are withheld is documented rather than hidden, so refusing before the compare gives nothing away that the manual does not. Selected by entity (get) — none — not a record; it names a kind of record, and every kind it admits is a kind that one shared secret reachesextension/preflight/feed/preflight — anybody holding the feed secret: The secret is the whole of the authentication on a URL that hands over a catalog export, and it travels in the query string, where a proxy log or a browser history keeps it. Selected by secret (get) — none — not a record |
8.3.1 |
What bounds a caller to their own records comes from the server — a session, a stored row, the store id — and never from a value the caller supplied. | declared — 5 distinct bounds, each named by the triple it scopes: bounded by Entities::fed(), which withholds orders, customers and coupons whatever secret is presentedbounded by the SAPI test; there is no per-record partition, because somebody at the shell is already the store bounded by the dispatched $cron_idbounded by the dispatched $cron_id, which nothing reachable over HTTP can supply — or the cli SAPI, which no request can present eitherbounded by the stored Configuration::FEED_SECRET, compared in constant time |
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 — 159 .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, operator — never from anything inside the secret presented:system/library/feed_access.php:35 — operatoradmin/controller/module/preflight.php:2872 — 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 — 159 .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 — 159 .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 — 159 .php files |
12.2.1 |
An outbound request is made over TLS with the certificate verified, and never falls back to cleartext. | not met — 159 .php files:extensions/preflight/src/system/library/port/url_fetcher.php:321 — the protocol allowlist admits CURLPROTO_HTTP, so an http:// URL is fetched in cleartext |
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 — 159 .php files |
14.2.1 |
A credential is not carried in a URL, where a browser history, a referrer header and a proxy log each keep their own copy of it. | not met — 2 bearer-secret surfaces of 2 travel in a URL:system/library/feed_access.php:35 — the secret query parameter on a public catalog route, over whatever transport the store is served on — so a proxy log, a referrer or a browser history keeps a copyadmin/controller/module/preflight.php:2872 — 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 — 6 .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 — 159 .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 — 36 store-derived subtrees reaches a template of this extension, each one written down; what a model row holds beyond them does not:mapping — supplier and operator: the column names the uploaded file carried, against the fields the operator bound them to, with their transforms and argumentssamples — supplier: the first rows of the uploaded file, printed verbatim so the operator can see what they are about to importops — store and supplier: one row per planned change, carrying the value the catalog holds and the value the feed would writefields — store: the field names the plan touches, off the entity's own SHOW COLUMNS, with a record count eachanomalies — store and supplier: the sentences naming what looks wrong about a plan, each quoting the figures it is aboutfound — store: the records a search matched, each carrying its own record label out of the catalogneedle — operator: the search term, echoed back into the formrollback — store: the journal of one job — what was restored, deleted or recreated, with the before and after of every field it touchedsummary — store: the seven counts a job ended with, read back off the job rowprogress — store: where a running job has got to, read back off the job rowhistory — store: past jobs, each carrying the name of the file it read and the operator who started itdrafts — store: mappings saved and not yet run, each under a name the operator typedprofiles — operator: saved profiles, each under a name the operator typed. The same names reach the store log undecoded from the cron route — see the cron/preflight.schedule entry under routesruns — store and operator: what each scheduled run did, carrying the profile's name and, where it failed, the exception's own messageestimate — store: how long a job of this size is likely to take on this store, measured from its own past runserror_warning — store: the sentence explaining a refusal, which for a PreflightException carries whatever the exception named — a directory, a column, a value out of the filesuccess — store: the sentence confirming what was done, which names the profile or the job it was done toroot — operator: the directory path the operator typed into the settings card, or the shipped defaultexports — operator: the export directory path the operator typed into the settings cardentity — operator: the kind of record chosen on the screen, narrowed to Entities::has() before it is usedentity_note — store: the sentence this screen prints about the chosen entity, which is a language string and empty where there is nonekey_field — store: the entity's own primary key column namematch_fields — store: the columns of the chosen entity a row may be matched onexport_groups — store: every field of the chosen entity, grouped, off the field catalog the database's own SHOW COLUMNS builtexport_sorts — store: the fields of the chosen entity an export may be ordered byexport_filters — store: the fields of the chosen entity an export may be narrowed byexport — store: the export profile being edited — its name, fields, format and filters as saved in preflight_profile — or, on a fresh form, the matchable columns of the chosen entity, preselected — only model and SKU for a productexport_languages — store: the names of the languages the store has installedentities — store: the entity options, each with the label the store's own language file gives itfeed_url — store: HTTP_CATALOG out of the store's own configuration, with the chosen entity appendedai_provider — operator: which provider the operator chose. The key itself is never sent to the screen — ai_key_set is a boolean saying whether one is stored, and nothing moreai_model — store: the model that provider is being asked for, derived from the chosen providerversion_status — store: what Compatibility::status() makes of the store's own VERSIONversion_warning — store: the sentence naming the store's own OpenCart version where it is outside the tested rangedownload — store: the link to the finished export of the job on screen, built from the job idresolution — store: the language readout's own payload — the names of the languages the store has installed and which of them this extension ships — handed to the second view file directly rather than through render() |
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 — 159 .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 — 159 .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. | not met — 159 .php files:extensions/preflight/src/system/library/port/database_store.php:4262 — a caught Exception message is concatenated into a thrown exception, carrying whatever it said — for a database driver, the failing statement |