Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,21 @@ jobs:
matrix:
php: ['7.4', '8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
with:
php-version: ${{ matrix.php }}
- name: Lint PHP files
run: find . -type f -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l

browser-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm test
67 changes: 67 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Build release candidate

on:
workflow_dispatch:
pull_request:
push:
tags: ['v*']

permissions:
contents: read

jobs:
transport-package:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: modx
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5
with:
php-version: '8.2'
extensions: pdo_mysql, zip, mbstring, json
tools: composer:v2
- name: Install pinned MODX build runtime
run: composer create-project modx/revolution /tmp/modx 3.2.3-pl --no-interaction --prefer-dist
- name: Install MODX non-interactively
run: |
cp tests/fixtures/modx-config.xml /tmp/modx/setup/config.xml
php /tmp/modx/setup/index.php --installmode=new --config=/tmp/modx/setup/config.xml
- name: Build twice and verify reproducibility
run: |
cp _build/build.config.sample.php _build/build.config.php
mkdir -p dist
MODX_BASE_PATH=/tmp/modx/ php _build/build.transport.php
python3 scripts/normalize-transport.py /tmp/modx/core/packages/amwebconsent-1.0.0-rc2.transport.zip /tmp/first.transport.zip
MODX_BASE_PATH=/tmp/modx/ php _build/build.transport.php
python3 scripts/normalize-transport.py /tmp/modx/core/packages/amwebconsent-1.0.0-rc2.transport.zip /tmp/second.transport.zip
cmp /tmp/first.transport.zip /tmp/second.transport.zip
cp /tmp/first.transport.zip dist/amwebconsent-1.0.0-rc2.transport.zip
sha256sum dist/amwebconsent-1.0.0-rc2.transport.zip > dist/amwebconsent-1.0.0-rc2.transport.zip.sha256
- name: Verify package lifecycle on clean MODX
run: MODX_BASE_PATH=/tmp/modx/ php tests/integration/package-lifecycle.php "$PWD/dist/amwebconsent-1.0.0-rc2.transport.zip"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: amwebconsent-transport
path: dist/*
if-no-files-found: error
- name: Attest build provenance
if: startsWith(github.ref, 'refs/tags/')
uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
with:
subject-path: dist/amwebconsent-1.0.0-rc2.transport.zip
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/.DS_Store
/_build/build.config.php
/dist/
/node_modules/
/playwright-report/
/test-results/
/vendor/
15 changes: 15 additions & 0 deletions .planning/PROJECT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# amWeb Consent (powered by Klaro)

Open-source MODX 3 Extra by amWeb that integrates the vendored Klaro consent manager. The project targets MODX 3.x and PHP 7.4+, is licensed under MIT, and is hosted at https://github.com/amWeb-DevTeam/amweb-consent.

## Product constraints

- Consent-controlled services must not contact third parties before opt-in.
- Runtime paths come from MODX constants/options, not editable component settings.
- Dynamic JavaScript values use JSON encoding.
- Strict CSP deployments can pass a nonce to every generated script.
- Release candidates are built and tested on GitHub before MODX submission.

## Current milestone

Prepare `1.0.0-rc2` as the security-hardened public release candidate.
12 changes: 12 additions & 0 deletions .planning/REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Requirements

## Release candidate 1.0.0-rc2

- [x] SEC-01 Escape reCAPTCHA values according to URL and JavaScript context.
- [x] SEC-02 Support nonce-based CSP and remove inline event handlers.
- [x] SEC-03 Reject unsupported MODX/PHP versions during installation.
- [x] SEC-04 Prove in a browser that blocked services make no request before consent.
- [x] SEC-05 Pin CI actions and produce a normalized, reproducible transport package.
- [x] REL-01 Pass the clean MODX install/uninstall job on GitHub Actions.
- [ ] REL-02 Review and merge the hardening pull request.
- [ ] REL-03 Publish and verify the `1.0.0-rc2` prerelease asset and checksum.
15 changes: 15 additions & 0 deletions .planning/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Roadmap

## Phase 1 — Public RC foundation (complete)

Repository, MIT licensing, English documentation, MODX 3 package structure, and `1.0.0-rc1` prerelease.

## Phase 2 — Security hardening (in progress)

Harden render contexts and paths, add CSP support and install validation, automate browser security tests, and make the release build reproducible.

Success criteria: SEC-01 through SEC-05 pass locally and REL-01 passes in GitHub Actions.

## Phase 3 — Official MODX submission (pending)

Publish the verified release candidate, collect acceptance feedback, then submit the Extra to the MODX provider.
25 changes: 25 additions & 0 deletions .planning/STATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Project state

- Milestone: `1.0.0-rc2`
- Active phase: 2 — Security hardening
- Active branch: `codex/security-hardening-rc2`
- Last verified: 2026-08-05
- Next action: review and merge draft PR #1, then publish `1.0.0-rc2`.

## Verified locally

- npm audit: 0 vulnerabilities
- Playwright: 3/3 passed
- PHP syntax: passed in the MODX DDEV runtime
- MODX transport build: passed twice
- Normalized artifacts: byte-identical
- Clean MODX 3.2.3 install/uninstall: passed in GitHub Actions run 30997904144
- Current artifact SHA-256: `fea322a43937f6945efd908a881463fac199a2aca2eede188ca849c2d1f321a9`

## Decisions

- Only MODX 3.x is supported.
- Minimum PHP is 7.4.
- Component `core_path` and `assets_url` settings are no longer trusted or used.
- Config chunks contain JavaScript only; the renderer owns the script tag and nonce.
- Playwright is pinned to 1.62.1 due to GHSA-7mvr-c777-76hp in older versions.
14 changes: 14 additions & 0 deletions .planning/quick/260805-sec-security-hardening-rc2/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Quick task: Security hardening for rc2

## Goal

Resolve the five security review findings with minimal process overhead while preserving verifiable release gates.

## Tasks

1. Harden context encoding and eliminate configurable runtime paths.
2. Add nonce-based CSP output and externalize the settings-link handler.
3. Add MODX/PHP package requirements and an install validator.
4. Add browser tests for consent network blocking and strict CSP.
5. Pin CI dependencies, normalize MODX randomness, and verify duplicate builds.
6. Add a clean MODX install/uninstall gate to the release workflow.
14 changes: 14 additions & 0 deletions .planning/quick/260805-sec-security-hardening-rc2/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Summary

All five requested hardening areas were implemented for `1.0.0-rc2`.

- JavaScript values are JSON-encoded; the reCAPTCHA URL key is URL-encoded.
- Generated scripts accept a CSP nonce; inline `onclick` was replaced by an external listener.
- Editable component path settings were removed from runtime use and package settings.
- Package metadata and a PHP validator enforce MODX 3.x, PHP 7.4+, and JSON support.
- Playwright proves no pre-consent/rejected network request and validates strict CSP operation.
- GitHub actions and Node test dependencies are pinned.
- MODX-generated random vehicle IDs, ZIP timestamps, metadata, and ordering are normalized.
- The release workflow builds twice, compares artifacts, tests clean install/uninstall, uploads a checksum, and emits provenance for tag builds.

The GitHub-hosted clean MODX lifecycle run passed. Only review/merge and publishing the release candidate remain.
15 changes: 15 additions & 0 deletions .planning/quick/260805-sec-security-hardening-rc2/VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Verification

Status: passed locally and in the hosted lifecycle gate.

| Gate | Result |
|---|---|
| npm audit | PASS — 0 vulnerabilities |
| Browser consent tests | PASS — 3/3 |
| Strict CSP browser test | PASS |
| PHP syntax | PASS |
| MODX package build | PASS |
| Reproducible normalized ZIP | PASS |
| Clean MODX install/uninstall | PASS — GitHub Actions run 30997904144 |

Artifact SHA-256: `fea322a43937f6945efd908a881463fac199a2aca2eede188ca849c2d1f321a9`
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 1.0.0-rc2

- Fix the generated configuration script so it contains real newlines instead of invalid literal `\\n` tokens.
- Use `amWeb Consent (powered by Klaro)` as the installed package display name.
- Safely JSON-encode dynamic JavaScript values and reCAPTCHA settings.
- Remove configurable PHP and asset paths from runtime loading.
- Add CSP nonce support and replace inline click handlers.
- Route split head/footer rendering through the secure PHP renderer.
- Add MODX 3, PHP 7.4 and JSON installation validation.
- Add browser tests proving trackers stay blocked before consent and rejection.
- Pin GitHub Actions and add reproducible package builds with provenance.

## 1.0.0-rc1

- First public release candidate as amWeb Consent (powered by Klaro).
Expand Down
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Snippets:

- `amWebConsent`
- `amWebConsentLink`
- `amWebConsentJson` (internal output filter)

Chunks:

Expand Down Expand Up @@ -69,8 +70,6 @@ System settings:
- `amwebconsent.load_css`
- `amwebconsent.load_js`
- `amwebconsent.debug`
- `amwebconsent.assets_url`
- `amwebconsent.core_path`

## Basic Installation

Expand Down Expand Up @@ -98,6 +97,14 @@ Split head/footer integration:
[[$amwebconsent.footer]]
```

The split chunks accept a request-specific `nonce` placeholder. Pass the same
nonce used in your Content Security Policy header:

```html
[[$amwebconsent.head? &nonce=`[[+cspNonce]]`]]
[[$amwebconsent.footer? &nonce=`[[+cspNonce]]`]]
```

Manual consent settings link:

```html
Expand All @@ -123,10 +130,12 @@ Button variant:

| Property | Default | Description |
| --- | --- | --- |
| `configChunk` | `amwebconsent.config` | Chunk that returns `window.klaroConfig`. |
| `configChunk` | `amwebconsent.config` | Chunk that returns JavaScript only; the snippet creates the script element. |
| `loadCss` | system setting | Load local Klaro CSS. |
| `loadJs` | system setting | Load local Klaro JS. |
| `renderConfig` | `1` | Render the Klaro configuration script. Set to `0` in footer-only calls. |
| `autoLoad` | `1` | If `0`, writes `noAutoLoad: true` to the config. |
| `nonce` | empty | Request-specific CSP nonce applied to every script element. |
| `debug` | system setting | Adds a small HTML comment and logs empty config output. |

Example:
Expand All @@ -137,17 +146,21 @@ Example:
&loadCss=`1`
&loadJs=`1`
&autoLoad=`1`
&nonce=`[[+cspNonce]]`
]]
```

Custom configuration chunks must return JavaScript only, without surrounding
`<script>` tags. Use JSON-encoded placeholders for every dynamic string.

### amWebConsentLink

| Property | Default | Description |
| --- | --- | --- |
| `text` | lexicon text | Visible link/button text. |
| `class` | `amwebconsent-link` | CSS class. |
| `tag` | `a` | `a` or `button`. |
| `forceModal` | `1` | Calls `klaro.show(undefined, true)`. |
| `forceModal` | `1` | Requests the full settings modal from the local event handler. |

## Purposes/Categories

Expand Down Expand Up @@ -195,6 +208,12 @@ Rules:
- `data-name` must match the Klaro service `name`.
- `data-src` replaces `src` for external scripts, iframes and images.

> Consent blocking is fail-open when integration markup is wrong: an ordinary
> `<script src>` or `<iframe src>` loads immediately. Test every production
> service in the browser network panel before deployment. The automated tests
> in this repository assert that correctly marked services make no request
> before consent.

## Matomo Cookieless Example

Enable the `matomo-cookieless` service in `amwebconsent.config` and add:
Expand Down Expand Up @@ -300,7 +319,7 @@ MODX_BASE_PATH=/absolute/path/to/modx/ php _build/build.transport.php
The package will be written to MODX `core/packages/`, for example:

```text
core/packages/amwebconsent-1.0.0-rc1.transport.zip
core/packages/amwebconsent-1.0.0-rc2.transport.zip
```

Install it through the MODX Package Manager.
Expand All @@ -318,6 +337,16 @@ assets/components/amwebconsent/css/klaro.min.css
Do not use uncompiled files from Klaro `src/` directly in the browser.
Update `THIRD_PARTY_NOTICES.md` and its checksums whenever these assets change.

## Security and Release Verification

Pull requests run PHP lint and browser security tests. Tagged builds use a
pinned MODX 3.2.3 runtime, build the package twice, normalize ZIP metadata and
fail when the two results differ. Release artifacts include a SHA-256 checksum
and GitHub build-provenance attestation.

The package validator refuses installation outside MODX 3.x, below PHP 7.4,
or without JSON support.

## Legal Notice

amWeb Consent provides technical integration and examples. It does not
Expand Down
4 changes: 4 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ versions, reproduction steps and the expected impact.

This Extra assists with technical consent handling. It is not legal advice and
does not make a site compliant without correct site-specific configuration.

Correct service markup is part of the security boundary. Scripts, iframes and
other remote resources that are not converted to Klaro's blocked `data-src`
form will load before consent. Verify the browser network log for every service.
1 change: 1 addition & 0 deletions _build/build.config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'core' => $root . 'core/components/amwebconsent/',
'assets' => $root . 'assets/components/amwebconsent/',
'data' => __DIR__ . '/data/',
'validators' => __DIR__ . '/validators/',
'resolvers' => __DIR__ . '/resolvers/',
'snippets' => $root . 'core/components/amwebconsent/elements/snippets/',
'chunks' => $root . 'core/components/amwebconsent/elements/chunks/',
Expand Down
11 changes: 9 additions & 2 deletions _build/build.transport.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
define('PKG_NAME', 'amWeb Consent');
define('PKG_NAME_LOWER', 'amwebconsent');
define('PKG_VERSION', '1.0.0');
define('PKG_RELEASE', 'rc1');
define('PKG_RELEASE', 'rc2');

$configFile = __DIR__ . '/build.config.php';
if (!is_file($configFile)) {
Expand Down Expand Up @@ -99,8 +99,11 @@ function amwebConsentReadSnippet(string $path): string
'source' => $sources['assets'],
'target' => "return MODX_ASSETS_PATH . 'components/';",
]);
$vehicle->validate('php', [
'source' => $sources['validators'] . 'validate.requirements.php',
]);
$vehicle->resolve('php', [
'source' => $sources['resolvers'] . 'resolve.settings.php',
'source' => $sources['resolvers'] . 'resolve.package-display-name.php',
]);

$builder->putVehicle($vehicle);
Expand All @@ -126,6 +129,10 @@ function amwebConsentReadSnippet(string $path): string
'license' => file_get_contents($sources['root'] . 'LICENSE.md'),
'readme' => file_get_contents($sources['root'] . 'README.md'),
'changelog' => file_get_contents($sources['root'] . 'CHANGELOG.md'),
'requires' => [
'modx' => '>=3.0.0,<4.0.0',
'php' => '>=7.4.0',
],
]);

$builder->pack();
Expand Down
2 changes: 0 additions & 2 deletions _build/data/transport.settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,4 @@
['key' => 'amwebconsent.load_css', 'value' => true, 'xtype' => 'combo-boolean', 'area' => 'amwebconsent.assets'],
['key' => 'amwebconsent.load_js', 'value' => true, 'xtype' => 'combo-boolean', 'area' => 'amwebconsent.assets'],
['key' => 'amwebconsent.debug', 'value' => false, 'xtype' => 'combo-boolean', 'area' => 'amwebconsent.general'],
['key' => 'amwebconsent.assets_url', 'value' => '{assets_url}components/amwebconsent/', 'xtype' => 'textfield', 'area' => 'amwebconsent.assets'],
['key' => 'amwebconsent.core_path', 'value' => '{core_path}components/amwebconsent/', 'xtype' => 'textfield', 'area' => 'amwebconsent.assets'],
];
Loading
Loading