You've already forked wp-bootstrap
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89afa00678 |
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"MD024": false,
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.0.8] - 2026-02-19
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **Archive XSS hardening**: `ContextBuilder::getArchiveData()` now wraps `get_the_archive_title()` and `get_the_archive_description()` with `wp_kses_post()`. Term descriptions are user-editable by Editors and above; without sanitization an injected `<script>` tag would execute via the `|raw` filter in `archive.html.twig`
|
||||||
|
- **Comment author XSS hardening**: `ContextBuilder::buildCommentTree()` now applies `esc_html()` to `comment_author` and `esc_url()` to `comment_author_url` at the data source, preventing injection via user-supplied comment fields
|
||||||
|
- **Dark mode localStorage whitelist**: `getPreferredTheme()` in `dark-mode.js` now validates the stored theme value against `['dark', 'light']` before use, preventing attribute injection from a tampered localStorage value written by a third-party script
|
||||||
|
- **Twig escaping functions marked safe**: `esc_html()`, `esc_attr()`, and `esc_url()` registered in `TwigService` are now declared with `['is_safe' => ['html']]`, preventing double-encoding if Twig autoescape is ever enabled
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `views/partials/comment-item.html.twig`: Comment author URL now output via `{{ comment.author_url|raw }}` (escaped in PHP) instead of calling `esc_url()` from the template, keeping escaping logic in one place
|
||||||
|
|
||||||
## [1.0.7] - 2026-02-18
|
## [1.0.7] - 2026-02-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
38
CLAUDE.md
38
CLAUDE.md
@@ -211,6 +211,44 @@ Build steps (in order):
|
|||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
|
### Session 14 — v1.0.8 Security Audit & Hardening (2026-02-19)
|
||||||
|
|
||||||
|
**Completed:** Comprehensive OWASP-aligned security audit. Two parallel background agents reviewed all PHP (functions.php, ContextBuilder, NavWalker, TemplateController, TwigService, all patterns) and JavaScript/Twig templates. Four targeted security fixes applied.
|
||||||
|
|
||||||
|
**Findings and fixes:**
|
||||||
|
|
||||||
|
- **Archive term description XSS (High)**: `get_the_archive_description()` returns raw term content editable by Editor-role users. Templates rendered it with `|raw`, creating a stored XSS path. Fixed: wrapped with `wp_kses_post()` in `ContextBuilder::getArchiveData()`. Same applied to `get_the_archive_title()`.
|
||||||
|
- **Comment author injection (Low, defense-in-depth)**: `comment_author` and `comment_author_url` were passed to Twig as raw database values. Fixed: `esc_html()` applied to author name, `esc_url()` applied to author URL in `ContextBuilder::buildCommentTree()`. Template updated to output pre-escaped URL via `|raw` rather than calling `esc_url()` in Twig.
|
||||||
|
- **Dark mode localStorage whitelist (Medium)**: `getPreferredTheme()` returned any stored value without validation, allowing attribute injection if a malicious script wrote to `localStorage`. Fixed: strict equality check against `['dark', 'light']` before trusting the stored value.
|
||||||
|
- **Twig escaping functions not marked safe (Medium)**: `esc_html()`, `esc_attr()`, `esc_url()` registered in `TwigService` lacked `['is_safe' => ['html']]`, meaning any future autoescape enablement would cause double-encoding. Fixed: all three now carry the `is_safe` declaration.
|
||||||
|
|
||||||
|
**Confirmed secure (no action needed):**
|
||||||
|
|
||||||
|
- All `|raw` filter usages for widget HTML, comment content, comment reply links, and comment forms are by-design (WordPress core output)
|
||||||
|
- Pattern files: no direct-access guards needed (loaded only via `register_block_pattern()`)
|
||||||
|
- No SQL injection vectors (`$wpdb` not used directly; all data via WordPress functions)
|
||||||
|
- `TemplateController` error handling: `\Throwable` caught, logged, and gated behind `WP_DEBUG`
|
||||||
|
- `do_shortcode()` and `wp_kses_post()` Twig functions correctly marked `is_safe`
|
||||||
|
- `wp_head()`, `wp_footer()`, `body_class()` Twig functions correctly use output buffering + `is_safe`
|
||||||
|
|
||||||
|
**Key learnings:**
|
||||||
|
|
||||||
|
- WordPress Twig themes should not enable `autoescape => 'html'` globally: `get_the_title()` applies `wptexturize()` which returns HTML entities (`—`, `“`). Autoescape would double-encode these, corrupting post title rendering.
|
||||||
|
- `esc_url()` does more than HTML-encoding — it validates the URL scheme and strips dangerous protocols (`javascript:`, `data:`). Always use it for user-supplied URLs, even when autoescape is active.
|
||||||
|
- Registering WordPress escaping functions (`esc_url`, `esc_html`, `esc_attr`) as Twig functions without `is_safe => html` silently creates a double-encoding trap: calling `{{ esc_url(url) }}` with autoescape on would produce `&amp;` instead of `&`.
|
||||||
|
- Added `.markdownlint.json` disabling MD024 (duplicate headings, expected in changelogs) and MD013 (line length).
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
|
||||||
|
- `inc/Template/ContextBuilder.php` — archive data sanitization, comment field escaping
|
||||||
|
- `inc/Twig/TwigService.php` — `is_safe => html` on three escaping functions
|
||||||
|
- `views/partials/comment-item.html.twig` — use pre-escaped author URL
|
||||||
|
- `src/js/dark-mode.js` — localStorage whitelist
|
||||||
|
- `assets/js/dark-mode.js` — rebuilt compiled output
|
||||||
|
- `style.css` — version bump to 1.0.8
|
||||||
|
- `CHANGELOG.md` — v1.0.8 entry
|
||||||
|
- `.markdownlint.json` — created
|
||||||
|
|
||||||
### Session 13 — v1.0.5 Translation Files (2026-02-11)
|
### Session 13 — v1.0.5 Translation Files (2026-02-11)
|
||||||
|
|
||||||
**Completed:** Standardized translation file naming and added 11 new locale translations.
|
**Completed:** Standardized translation file naming and added 11 new locale translations.
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
function getPreferredTheme() {
|
function getPreferredTheme() {
|
||||||
var stored = localStorage.getItem(STORAGE_KEY);
|
var stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (stored) {
|
// Whitelist: only honour known-good values to prevent attribute injection
|
||||||
|
// from a tampered localStorage (e.g. XSS-written value by another script).
|
||||||
|
if (stored === 'dark' || stored === 'light') {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
|||||||
@@ -245,8 +245,10 @@ class ContextBuilder
|
|||||||
private function getArchiveData(): array
|
private function getArchiveData(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'title' => get_the_archive_title(),
|
// wp_kses_post() allows safe HTML (headings, links, spans) while stripping
|
||||||
'description' => get_the_archive_description(),
|
// script/event-handler attributes that could be injected via term descriptions.
|
||||||
|
'title' => wp_kses_post(get_the_archive_title()),
|
||||||
|
'description' => wp_kses_post(get_the_archive_description()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,8 +293,10 @@ class ContextBuilder
|
|||||||
|
|
||||||
$tree[] = [
|
$tree[] = [
|
||||||
'id' => (int) $comment->comment_ID,
|
'id' => (int) $comment->comment_ID,
|
||||||
'author' => $comment->comment_author,
|
// Escape at source — comment_author is user-supplied, store as safe text.
|
||||||
'author_url' => $comment->comment_author_url,
|
'author' => esc_html($comment->comment_author),
|
||||||
|
// esc_url() strips dangerous schemes (javascript:, data:) and encodes for HTML.
|
||||||
|
'author_url' => esc_url($comment->comment_author_url),
|
||||||
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
|
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
|
||||||
'date' => get_comment_date('', $comment),
|
'date' => get_comment_date('', $comment),
|
||||||
'date_iso' => get_comment_date('c', $comment),
|
'date_iso' => get_comment_date('c', $comment),
|
||||||
|
|||||||
@@ -73,10 +73,12 @@ class TwigService
|
|||||||
return _n($single, $plural, $number, $domain);
|
return _n($single, $plural, $number, $domain);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Escaping functions.
|
// Escaping functions — marked is_safe so Twig does not double-escape their output.
|
||||||
$this->twig->addFunction(new TwigFunction('esc_html', 'esc_html'));
|
// These functions already return HTML-safe strings; without is_safe, enabling
|
||||||
$this->twig->addFunction(new TwigFunction('esc_attr', 'esc_attr'));
|
// Twig autoescape would double-encode the result (e.g. & → &amp;).
|
||||||
$this->twig->addFunction(new TwigFunction('esc_url', 'esc_url'));
|
$this->twig->addFunction(new TwigFunction('esc_html', 'esc_html', ['is_safe' => ['html']]));
|
||||||
|
$this->twig->addFunction(new TwigFunction('esc_attr', 'esc_attr', ['is_safe' => ['html']]));
|
||||||
|
$this->twig->addFunction(new TwigFunction('esc_url', 'esc_url', ['is_safe' => ['html']]));
|
||||||
|
|
||||||
// WordPress head/footer output (captured via output buffering).
|
// WordPress head/footer output (captured via output buffering).
|
||||||
$this->twig->addFunction(new TwigFunction('wp_head', function (): string {
|
$this->twig->addFunction(new TwigFunction('wp_head', function (): string {
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
*/
|
*/
|
||||||
function getPreferredTheme() {
|
function getPreferredTheme() {
|
||||||
var stored = localStorage.getItem(STORAGE_KEY);
|
var stored = localStorage.getItem(STORAGE_KEY);
|
||||||
if (stored) {
|
// Whitelist: only honour known-good values to prevent attribute injection
|
||||||
|
// from a tampered localStorage (e.g. XSS-written value by another script).
|
||||||
|
if (stored === 'dark' || stored === 'light') {
|
||||||
return stored;
|
return stored;
|
||||||
}
|
}
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Description: A modern WordPress Block Theme built from scratch with Bootstrap 5.
|
|||||||
Requires at least: 6.7
|
Requires at least: 6.7
|
||||||
Tested up to: 6.7
|
Tested up to: 6.7
|
||||||
Requires PHP: 8.3
|
Requires PHP: 8.3
|
||||||
Version: 1.0.7
|
Version: 1.0.8
|
||||||
License: GNU General Public License v2 or later
|
License: GNU General Public License v2 or later
|
||||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||||
Text Domain: wp-bootstrap
|
Text Domain: wp-bootstrap
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
<div class="d-flex align-items-center gap-2 mb-1">
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
<strong class="small">
|
<strong class="small">
|
||||||
{% if comment.author_url %}
|
{% if comment.author_url %}
|
||||||
<a href="{{ esc_url(comment.author_url) }}" class="text-decoration-none text-body" rel="nofollow">
|
{# author_url is pre-escaped with esc_url() in ContextBuilder #}
|
||||||
|
<a href="{{ comment.author_url|raw }}" class="text-decoration-none text-body" rel="nofollow">
|
||||||
{{ comment.author }}
|
{{ comment.author }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
Reference in New Issue
Block a user