Skip to content

feat(security): use richdocuments.federation_allowlist system config for federation server validation#599

Open
DeepDiver1975 wants to merge 15 commits into
masterfrom
feat/federation-allowlist-config
Open

feat(security): use richdocuments.federation_allowlist system config for federation server validation#599
DeepDiver1975 wants to merge 15 commits into
masterfrom
feat/federation-allowlist-config

Conversation

@DeepDiver1975
Copy link
Copy Markdown
Member

@DeepDiver1975 DeepDiver1975 commented May 15, 2026

Summary

  • Replaces `OCA\Federation\TrustedServers` with a plain PHP array in `config.php` as the sole authority for permitted federation servers
  • Admins configure the allowlist via `richdocuments.federation_allowlist` system config key — no federation app dependency required
  • Absent, empty, or non-array values deny all federation requests (secure default)
  • Allowlist entries are plain domain names (no scheme); both http and https requests are accepted for any listed domain

Config

```php
// config/config.php
'richdocuments.federation_allowlist' => [
'collab1.example.com',
'collab2.example.com',
],
```

Test plan

  • Unit tests cover: absent key, non-array value, exact match, trailing slash (single/multiple), https request, http request, untrusted server
  • `make test-php-style` passes
  • CI: PHP Code Style, Phan, PHPStan all green
  • Integration: add entry to `config.php`, verify trusted remote works; verify unlisted remote is denied with log entry
  • Integration: omit key entirely, verify all federation requests denied

🤖 Generated with Claude Code

DeepDiver1975 and others added 11 commits May 12, 2026 16:28
Replaces the always-true isServerAllowed() stub with a real check against
ownCloud's federation trusted-servers list (OCA\Federation\TrustedServers).

When TrustedServers is null (federation app not installed), all remote
federation requests are denied. URL comparison strips trailing slashes and
checks both http/https scheme variants to tolerate minor admin-input
differences.

Fixes OC10-100 (SSRF via federated endpoint, CVSS 7.1).

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Explicitly register FederationService in the app container so that
TrustedServers (from the federation app) is injected when available,
and null is passed when the federation app is not installed.

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
… test

- Application.php: replace \OC::$server->query() with the already-captured
  $server variable for consistency and testability
- FederationServiceTest: remove testIsServerAllowedReturnsFalseWhenListIsEmpty
  which was identical to testIsServerAllowedReturnsFalseForUntrustedServer

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…tedServers

The federation app is an optional runtime dependency not present in
Phan's analysis path. Suppress PhanUndeclaredClass* at the five sites
that reference TrustedServers, matching the project's established
inline-suppression pattern.

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…ration line

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…allowlist system config

isServerAllowed() now reads a PHP array from config.php via
richdocuments.federation_allowlist. Absent/empty/non-array values deny all.
Each entry is checked with trailing-slash stripping and http/https scheme
swap to tolerate minor URL mismatches.

Removes the optional federation-app dependency entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
No longer needs the federation app isInstalled() check or Phan
suppression. IConfig is a core ownCloud interface, always available.

Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
@DeepDiver1975 DeepDiver1975 requested a review from jvillafanez May 15, 2026 07:23
@DeepDiver1975
Copy link
Copy Markdown
Member Author

Code Review

Overview

This PR replaces the OCA\Federation\TrustedServers dependency from PR #598 with a standalone richdocuments.federation_allowlist system config array, removing the hard runtime dependency on the federation app being installed. Admins configure allowed federation endpoints directly in config.php.

3 PHP files changed + 2 documentation files added.


lib/FederationService.php

Clean. The isServerAllowed() rewrite is correct:

  • Deny-all default when key is absent or non-array ✓
  • Trailing-slash stripping on both $remote and each allowlist entry ✓
  • HTTP↔HTTPS scheme swapping to tolerate minor mismatches ✓
  • No Phan suppression comments needed (IConfig is a core interface) ✓

lib/AppInfo/Application.php

Clean. DI registration simplified to four core dependencies; the isInstalled('federation') conditional and null-TrustedServers pattern from PR #598 are gone entirely.


tests/unit/FederationServiceTest.php

9 focused tests covering all branches of isServerAllowed(). The IConfig mock setup is correct.

Minor: testIsServerAllowedReturnsFalseWhenKeyIsAbsent and testIsServerAllowedReturnsFalseWhenListIsEmpty are cosmetically redundant — both stub getSystemValue to return [] and both assert false. The distinction (absent key vs. explicitly empty array) is meaningful to document, so keeping both is reasonable; just worth noting they exercise identical code paths.


Overall

No security issues, no correctness issues, no style concerns. The implementation is minimal and correct. The only question is whether the cosmetic test redundancy warrants removing one test — I'd lean toward keeping both as documentation of intent.

Copy link
Copy Markdown
Member

@jvillafanez jvillafanez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I had in mind.

Comment thread lib/FederationService.php Outdated
Comment on lines 140 to 151
// swap scheme and try again
if (\strpos($normalized, 'https://') === 0) {
$swapped = 'http://' . \substr($normalized, 8);
} elseif (\strpos($normalized, 'http://') === 0) {
$swapped = 'https://' . \substr($normalized, 7);
} else {
return false;
$swapped = null;
}

foreach ($allowlist as $entry) {
$e = \rtrim($entry, '/');
if ($normalized === $e) {
return true;
}
if ($swapped !== null && $swapped === $e) {
return true;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't care about the protocol, it might be easier to just ask for the trusted host (just oc.server.prv instead of https://oc.server.prv). Stripping the protocol from the incoming remote and then checking for the matching host should be easier, even if we would need to ensure it's http or https.

@DeepDiver1975 DeepDiver1975 changed the base branch from fix/oc10-100-ssrf-federation-trusted-servers to master May 15, 2026 07:45
DeepDiver1975 and others added 2 commits May 15, 2026 09:50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Allowlist entries are now plain domain names (e.g. 'cloud.example.com')
without a scheme. isServerAllowed() strips the http/https scheme from
the incoming URL before matching, so both http and https requests are
accepted for any listed domain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
@DeepDiver1975
Copy link
Copy Markdown
Member Author

Code Review

Overview

Replaces the isServerAllowed() stub (returned true unconditionally) with a real implementation backed by a richdocuments.federation_allowlist system config array of plain domain names. The scheme is stripped from the incoming URL before matching, so a single entry covers both http:// and https://. DI wiring and tests updated accordingly.


lib/FederationService.php

Correct and clean. Two observations:

  • preg_replace null return: preg_replace() can return null on error. With a literal pattern this is impossible in practice, but a (string) cast makes the type intent explicit and silences static analysis:

    $domain = \rtrim((string)\preg_replace('|^https?://|', '', $remote), '/');
  • Path-prefixed installs: After scheme stripping, https://cloud.example.com/owncloud becomes cloud.example.com/owncloud and won't match a bare domain entry. Worth a note in the docblock as a known limitation.


tests/unit/FederationServiceTest.php

Good coverage. One cleanup: testIsServerAllowedReturnsFalseWhenKeyIsAbsent and testIsServerAllowedReturnsFalseWhenListIsEmpty both stub getSystemValue to return [] and assert false — identical code paths. One can be dropped.


PR Description

Stale — needs updating:

  1. Config example still shows scheme-prefixed URLs (https://collab1.example.com) — should be plain domain names.
  2. Test plan still mentions http→https swap / https→http swap — that logic was removed.

Security

No issues. Domain-only matching is slightly more robust than the previous scheme-swap approach.


Summary

One code fix ((string) cast), PR description update, and one redundant test to drop.

…h-prefix limitation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Copy link
Copy Markdown
Member

@jvillafanez jvillafanez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small thing, but it looks good.

Comment thread lib/AppInfo/Application.php Outdated
return $server->getL10N($c->query('AppName'));
});

$container->registerService(FederationService::class, function () use ($server) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is needed because it should be auto-wired by the DI

@DeepDiver1975 DeepDiver1975 marked this pull request as ready for review May 15, 2026 10:42
ownCloud's SimpleContainer autowires constructor dependencies by type
via reflection, so the manual registerService is redundant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants