From 234fd1edacce88f454ecb8350b80270c7281250c Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Fri, 6 Nov 2026 11:30:05 +0100 Subject: [PATCH] fix(security): Pods-Meta nur lesbar, wenn Pods aktiv, aber nicht nutzbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ist Pods aktiv, seine API aber nicht nutzbar, melden gesampelte und registrierte Meta-Schlüssel READONLY statt schreibbar. Sonst würden Pods-Felder (auch über Pods' register_meta-Integration) an Pods vorbei geschrieben. Co-Authored-By: Claude Opus 5.5 --- .../WordPress/Schema/RegisteredMetaSource.php | 7 ++++++- .../WordPress/Schema/SampledMetaSource.php | 11 +++++++++-- src/Plugin.php | 5 ++++- .../WordPress/Schema/RegisteredMetaSourceTest.php | 10 ++++++++++ .../WordPress/Schema/SampledMetaSourceTest.php | 10 ++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Integrations/WordPress/Schema/RegisteredMetaSource.php b/src/Integrations/WordPress/Schema/RegisteredMetaSource.php index c3ea9b1..2a7e6a0 100644 --- a/src/Integrations/WordPress/Schema/RegisteredMetaSource.php +++ b/src/Integrations/WordPress/Schema/RegisteredMetaSource.php @@ -13,6 +13,11 @@ final class RegisteredMetaSource extends MetaFieldSource { private const TYPES = ['string', 'boolean', 'integer', 'number', 'array', 'object']; + /** @param bool $readOnly Report every key as READONLY (Pods is active but cannot be used to write its keys). */ + public function __construct(private bool $readOnly = false) + { + } + public function id(): string { return 'meta'; @@ -26,7 +31,7 @@ final class RegisteredMetaSource extends MetaFieldSource $key = (string) $key; $args = is_array($args) ? $args : []; $type = (string) ($args['type'] ?? 'string'); - $readonly = is_protected_meta($key, 'post') && empty($args['show_in_rest']); + $readonly = $this->readOnly || (is_protected_meta($key, 'post') && empty($args['show_in_rest'])); $fields[] = new FieldDefinition( $key, diff --git a/src/Integrations/WordPress/Schema/SampledMetaSource.php b/src/Integrations/WordPress/Schema/SampledMetaSource.php index 2ed8d3a..17ee2b6 100644 --- a/src/Integrations/WordPress/Schema/SampledMetaSource.php +++ b/src/Integrations/WordPress/Schema/SampledMetaSource.php @@ -15,7 +15,8 @@ final class SampledMetaSource extends MetaFieldSource private const SAMPLE_POSTS = 50; private const MAX_KEYS = 200; - public function __construct(private ?\wpdb $db = null) + /** @param bool $readOnly Report the keys as READONLY (Pods is active but cannot be used to write them). */ + public function __construct(private ?\wpdb $db = null, private bool $readOnly = false) { } @@ -48,7 +49,13 @@ final class SampledMetaSource extends MetaFieldSource continue; } - $fields[] = new FieldDefinition($key, $key, 'unknown', 'sample', FieldDefinition::UNKNOWN); + $fields[] = new FieldDefinition( + $key, + $key, + 'unknown', + 'sample', + $this->readOnly ? FieldDefinition::READONLY : FieldDefinition::UNKNOWN + ); } return $fields; diff --git a/src/Plugin.php b/src/Plugin.php index 771e35a..58446e6 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -192,7 +192,10 @@ final class Plugin private static function contentSchema(): ContentSchema { - $sources = [new RegisteredMetaSource(), new SampledMetaSource()]; + // Pods active but unusable: its meta keys (sampled, or registered via Pods' register_meta integration) must not + // be written past Pods. + $podsUnusable = defined('PODS_VERSION') && ! PodsFieldSource::isAvailable(); + $sources = [new RegisteredMetaSource(readOnly: $podsUnusable), new SampledMetaSource(readOnly: $podsUnusable)]; if (PodsFieldSource::isAvailable()) { array_unshift($sources, new PodsFieldSource()); } diff --git a/tests/Unit/Integrations/WordPress/Schema/RegisteredMetaSourceTest.php b/tests/Unit/Integrations/WordPress/Schema/RegisteredMetaSourceTest.php index b00cf0a..50f6264 100644 --- a/tests/Unit/Integrations/WordPress/Schema/RegisteredMetaSourceTest.php +++ b/tests/Unit/Integrations/WordPress/Schema/RegisteredMetaSourceTest.php @@ -43,6 +43,16 @@ final class RegisteredMetaSourceTest extends TestCase self::assertFalse($fields['footnotes']['multiple']); } + public function testReadOnlyMakesEveryRegisteredKeyReadOnly(): void + { + Functions\when('get_registered_meta_keys')->justReturn(['city' => ['type' => 'string']]); + Functions\when('is_protected_meta')->justReturn(false); + + $fields = (new RegisteredMetaSource(readOnly: true))->fieldsFor('lit_person'); + + self::assertSame(FieldDefinition::READONLY, $fields[0]->status); + } + public function testProtectedMetaHiddenFromRestIsReadonly(): void { $fields = $this->fieldsByKey(); diff --git a/tests/Unit/Integrations/WordPress/Schema/SampledMetaSourceTest.php b/tests/Unit/Integrations/WordPress/Schema/SampledMetaSourceTest.php index d1fef9b..a1abfea 100644 --- a/tests/Unit/Integrations/WordPress/Schema/SampledMetaSourceTest.php +++ b/tests/Unit/Integrations/WordPress/Schema/SampledMetaSourceTest.php @@ -26,4 +26,14 @@ final class SampledMetaSourceTest extends TestCase self::assertSame('sample', $fields[0]->source); self::assertSame('unknown', $fields[0]->type); } + + public function testReadOnlyMakesEveryKeyReadOnly(): void + { + $db = Mockery::mock(\wpdb::class)->makePartial(); + $db->expects('get_col')->andReturn(['city']); + + $fields = (new SampledMetaSource($db, readOnly: true))->fieldsFor('lit_person'); + + self::assertSame(FieldDefinition::READONLY, $fields[0]->status); + } }