fix(security): Pods-Meta nur lesbar, wenn Pods aktiv, aber nicht nutzbar

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 <noreply@anthropic.com>
This commit is contained in:
Jan-Niclas Loosen
2026-11-06 11:30:05 +01:00
co-authored by Claude Opus 5.5
parent 8f3a4e76eb
commit 234fd1edac
5 changed files with 39 additions and 4 deletions
@@ -13,6 +13,11 @@ final class RegisteredMetaSource extends MetaFieldSource
{ {
private const TYPES = ['string', 'boolean', 'integer', 'number', 'array', 'object']; 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 public function id(): string
{ {
return 'meta'; return 'meta';
@@ -26,7 +31,7 @@ final class RegisteredMetaSource extends MetaFieldSource
$key = (string) $key; $key = (string) $key;
$args = is_array($args) ? $args : []; $args = is_array($args) ? $args : [];
$type = (string) ($args['type'] ?? 'string'); $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( $fields[] = new FieldDefinition(
$key, $key,
@@ -15,7 +15,8 @@ final class SampledMetaSource extends MetaFieldSource
private const SAMPLE_POSTS = 50; private const SAMPLE_POSTS = 50;
private const MAX_KEYS = 200; 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; 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; return $fields;
+4 -1
View File
@@ -192,7 +192,10 @@ final class Plugin
private static function contentSchema(): ContentSchema 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()) { if (PodsFieldSource::isAvailable()) {
array_unshift($sources, new PodsFieldSource()); array_unshift($sources, new PodsFieldSource());
} }
@@ -43,6 +43,16 @@ final class RegisteredMetaSourceTest extends TestCase
self::assertFalse($fields['footnotes']['multiple']); 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 public function testProtectedMetaHiddenFromRestIsReadonly(): void
{ {
$fields = $this->fieldsByKey(); $fields = $this->fieldsByKey();
@@ -26,4 +26,14 @@ final class SampledMetaSourceTest extends TestCase
self::assertSame('sample', $fields[0]->source); self::assertSame('sample', $fields[0]->source);
self::assertSame('unknown', $fields[0]->type); 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);
}
} }