fix(sys-front-inj-matomo): keep the admin token out of the request URL

The SitesManager.getSitesIdFromSiteUrl lookup carried token_auth in the query
string of a uri_retry whose default is 30 attempts, against
matomo_index_php_url - which resolves to the public Matomo URL, not an
internal one. One unresolved site therefore wrote up to 31 lines holding the
administrator token into Matomo's own access log, the reverse-proxy access
log, and any URL-based telemetry on the path. no_log cannot reach those: it
masks Ansible's output, not files on the managed host, and local deploys run
with MASK_CREDENTIALS_IN_LOGS false anyway. The call now uses the POST form
body its siblings in web-app-matomo/tasks/07_config.yml already use.
Query-string call sites across the repo: 1 -> 0.

Site resolution moves out of inject.yml into tasks/resolve_site.yml and runs
before the inj_enabled gate; inject_apply.yml is absorbed into it. That lets
matomo_site_id default to an empty string instead of staying undefined, so
main.yml gates injection on the resolved id rather than on the token's
presence, and a failed resolution disables injection instead of raising
'matomo_site_id is undefined' later.

addSite is tightened the same way. It built its body as a raw interpolated
string, which the uri module does not URL-encode, and it trusted HTTP 200 -
but Matomo answers API errors with 200 and a JSON mapping, so the following
set_fact raised "'dict object' has no attribute 'value'" at a confusing
place. It now sends the dict body and carries a failed_when on json.value,
which surfaces Matomo's own error text.

The tracking spec counted bare /setSiteId/ occurrences, which a README
excerpt, an admin page or a code sample satisfies, so a page carrying no
tracker could read as tracked. It counts the _paq.push call instead.

Residuals: whether Matomo accepts this read method over POST is not executed.
07_config.yml proves POST with a dict body against the same index.php for
write methods, but that is analogy. If it does not, no site ever resolves,
inj_enabled.matomo goes false, and every tracked site silently loses its
tracker - closed, but invisible. And no lint pins the fix, so a later call
site can put a credential back in a URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-02 17:51:54 +02:00
1 parent 1e8be113f3
commit 7309d50575
5 files changed
+100 -79

No files matched your search

+14 -23
View File
@@ -1,25 +1,16 @@
---
- name: "🔎 Check if site {{ domain }} is already registered at Matomo"
uri_retry:
url: "{{ matomo_index_php_url }}?module=API&method=SitesManager.getSitesIdFromSiteUrl&url={{ matomo_site_url }}&format=json&token_auth={{ matomo_effective_token
}}"
method: GET
return_content: true
status_code: 200
validate_certs: true
register: site_check
failed_when: false
- name: 📄 Load Matomo tracking JS template
set_fact:
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
- name: 🗜️ Collapse Matomo code into one-liner
set_fact:
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
- name: ➕ Append Matomo CSP hash
set_fact:
webserver_csp_hashes_extra_by_app: >-
{{ webserver_csp_hashes_extra_by_app | default({})
| add_csp_hash(application_id, 'script-src-elem', matomo_tracking_code_one_liner) }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔴 Disable Matomo tracking for '{{ domain }}' (API not ready, reapplied later)"
ansible.builtin.set_fact:
inj_enabled: "{{ inj_enabled | combine({'matomo': false}) }}"
when: >-
site_check.status is not defined or site_check.status != 200
or site_check.json is not defined or site_check.json is mapping
- name: "📝 Register site + inject Matomo tracking for '{{ domain }}'"
ansible.builtin.include_tasks: inject_apply.yml
when: >-
site_check.status is defined and site_check.status == 200
and site_check.json is defined and site_check.json is not mapping
changed_when: false
@@ -1,45 +0,0 @@
---
- name: 🔧 Set matomo_site_id to Null
set_fact:
matomo_site_id:
- name: 🔧 Set fact for site ID if site already exists
set_fact:
matomo_site_id: "{{ site_check.json[0].idsite }}"
when: "site_check.json is not mapping and (site_check.json | length) > 0"
changed_when: false
- name: ➕ Add site to Matomo and get ID if not exists
uri_retry:
url: "{{ matomo_index_php_url }}"
method: POST
body: "module=API&method=SitesManager.addSite&siteName={{ matomo_site_domain }}&urls={{ matomo_site_url }}&format=json&token_auth={{ matomo_effective_token }}"
body_format: form-urlencoded
status_code: 200
return_content: true
validate_certs: true
register: add_site
when: "matomo_site_id is not defined or matomo_site_id is none"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: 🔧 Set fact for site ID if site was added
set_fact:
matomo_site_id: "{{ add_site.json.value }}"
when: "matomo_site_id is not defined or matomo_site_id is none"
changed_when: false
- name: 📄 Load Matomo tracking JS template
set_fact:
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
- name: 🗜️ Collapse Matomo code into one-liner
set_fact:
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
- name: ➕ Append Matomo CSP hash
set_fact:
webserver_csp_hashes_extra_by_app: >-
{{ webserver_csp_hashes_extra_by_app | default({})
| add_csp_hash(application_id, 'script-src-elem', matomo_tracking_code_one_liner) }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
changed_when: false
+4 -11
View File
@@ -7,20 +7,13 @@
# the Matomo body snippet (avoiding 'matomo_site_id is undefined') and inject.yml is not run.
# See docs/contributing/design/exceptions.md — "Matomo Token Not Yet Available".
- name: "🔧 Resolve effective Matomo API token for '{{ domain }}'"
ansible.builtin.set_fact:
matomo_effective_token: >-
{{
(matomo_token_override | default('') | string | trim)
if (matomo_token_override | default('') | string | trim) != ''
else (lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim)
}}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔎 Resolve the Matomo site id for '{{ domain }}'"
include_tasks: resolve_site.yml
- name: "🔴 Disable Matomo in inj_enabled for '{{ domain }}' — token not yet available"
- name: "🔴 Disable Matomo in inj_enabled for '{{ domain }}' — no site id yet"
ansible.builtin.set_fact:
inj_enabled: "{{ inj_enabled | combine({'matomo': false}) }}"
when: matomo_effective_token == ""
when: matomo_site_id == ""
- name: "💉 Inject Matomo tracking for '{{ domain }}'"
include_tasks: inject.yml
@@ -0,0 +1,69 @@
---
- name: "🔧 Resolve effective Matomo API token for '{{ domain }}'"
ansible.builtin.set_fact:
matomo_effective_token: >-
{{
(matomo_token_override | default('') | string | trim)
if (matomo_token_override | default('') | string | trim) != ''
else (lookup('users', 'administrator').tokens['web-app-matomo'] | default('') | string | trim)
}}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔧 Start from an unresolved Matomo site id for '{{ domain }}'"
ansible.builtin.set_fact:
matomo_site_id: ""
- name: "🔎 Check if site {{ domain }} is already registered at Matomo"
uri_retry:
url: "{{ matomo_index_php_url }}"
method: POST
body_format: form-urlencoded
body:
module: API
method: SitesManager.getSitesIdFromSiteUrl
url: "{{ matomo_site_url }}"
format: json
token_auth: "{{ matomo_effective_token }}"
return_content: true
status_code: 200
validate_certs: true
register: site_check
failed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
when: matomo_effective_token != ""
- block:
- name: "🔧 Set fact for site ID if site already exists"
ansible.builtin.set_fact:
matomo_site_id: "{{ site_check.json[0].idsite }}"
when: (site_check.json | length) > 0
changed_when: false
- name: "➕ Add site to Matomo and get ID if not exists"
uri_retry:
url: "{{ matomo_index_php_url }}"
method: POST
body_format: form-urlencoded
body:
module: API
method: SitesManager.addSite
siteName: "{{ matomo_site_domain }}"
urls: "{{ matomo_site_url }}"
format: json
token_auth: "{{ matomo_effective_token }}"
status_code: 200
return_content: true
validate_certs: true
register: add_site
when: matomo_site_id == ""
failed_when: add_site.json is not defined or add_site.json.value is not defined
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔧 Set fact for site ID if site was added"
ansible.builtin.set_fact:
matomo_site_id: "{{ add_site.json.value }}"
when: matomo_site_id == ""
changed_when: false
when: >-
site_check.status is defined and site_check.status == 200
and site_check.json is defined and site_check.json is not mapping
@@ -13,6 +13,12 @@ const {
} = require("./_shared");
const { assertInjectedAssetLoadsWithoutCspBlock, gotoOnion } = require("./personas");
const SET_SITE_ID_CALL = /_paq\s*\.\s*push\s*\(\s*\[\s*['"]setSiteId['"]/g;
function countTrackers(html) {
return (String(html).match(SET_SITE_ID_CALL) || []).length;
}
test.use({ ignoreHTTPSErrors: true });
test.beforeEach(async ({ page }) => {
@@ -99,15 +105,22 @@ for (const target of matomoTargetRoles) {
html,
`Expected matomo tracker '_paq' marker in ${target.id} HTML body`
).toContain("_paq");
expect(
html,
`Expected matomo tracker URL ('matomo.php') in ${target.id} HTML body`
).toContain("matomo.php");
if (matomoCanonicalDomain) {
expect(
html,
`Expected matomo host '${matomoCanonicalDomain}' referenced in ${target.id} HTML body`
).toContain(matomoCanonicalDomain);
}
expect(
countTrackers(html),
`Exactly one Matomo tracker must reach ${target.id}. Two means the snippet was injected twice and every visit is double-counted.`
).toBe(1);
});
}