Skip to content
Open
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
11 changes: 7 additions & 4 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ jobs:
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
php-versions: [ '8.2', '8.4']
php-versions: [ '8.2', '8.5']
symfony-versions: [ '6.4.*', '7.4.*']
experimental: [false]
timeout-minutes: 15
name: PHP ${{ matrix.php-versions }} on Ubuntu latest. Experimental == ${{ matrix.experimental }}
name: PHP ${{ matrix.php-versions }} / Symfony ${{ matrix.symfony-versions }} on Ubuntu latest. Experimental == ${{ matrix.experimental }}
steps:
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- name: Checkout
uses: actions/checkout@master
- name: Install dependencies
run: composer install
- name: Install Symfony ${{ matrix.symfony-versions }}
run: |
composer require symfony/framework-bundle:${{ matrix.symfony-versions }} symfony/security-bundle:${{ matrix.symfony-versions }} --no-update --no-interaction
composer update --prefer-dist --no-interaction
continue-on-error: ${{ matrix.experimental }}
- id: checks
name: Run CI tests
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Unreleased
- Add `MduiChunk` to parse and expose `mdui:UIInfo` / `mdui:DisplayName` from AuthnRequest SAML extensions
- Add `Extensions::getMduiChunk()` and `Extensions::hasMduiChunk()`
- Raise minimum Symfony 6 support from 6.3 (EOL) to 6.4 LTS
- Pin `simplesamlphp/saml2` to `<4.20` to avoid class redeclaration conflict with `xml-common`
- Remove `irstea/phpcpd-shim` (source unreachable from CI runners)
- CI matrix updated to PHP 8.2/8.5 and Symfony 6.4/7.4

# 7.0.2
- Fix version constraint error in `symfony/templating` requirement

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Developed as part of the [OpenConext-Stepup Gateway][2] and related OpenConext-S
composer require surfnet/stepup-saml-bundle
```

How to install with SF6
How to install with Symfony 6.3+ and 7.x

1. Require the bundle in the composer.json (version 4.1.9 or higher)
2. Enable the bundle in `config/bundles.php` add to the return statement: `Surfnet\SamlBundle\SurfnetSamlBundle::class => ['all' => true],`
Expand Down
36 changes: 0 additions & 36 deletions ci/qa/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,6 @@ parameters:
count: 1
path: ../../src/SAML2/AuthnRequest.php

-
message: '#^Method Surfnet\\SamlBundle\\SAML2\\AuthnRequest\:\:getAuthenticationContextClassRef\(\) should return string\|null but returns mixed\.$#'
identifier: return.type
count: 1
path: ../../src/SAML2/AuthnRequest.php

-
message: '#^Method Surfnet\\SamlBundle\\SAML2\\AuthnRequest\:\:getDestination\(\) should return string but returns string\|null\.$#'
identifier: return.type
Expand Down Expand Up @@ -528,12 +522,6 @@ parameters:
count: 1
path: ../../src/SAML2/AuthnRequest.php

-
message: '#^Parameter \#1 \$array of function reset expects array\|object, mixed given\.$#'
identifier: argument.type
count: 1
path: ../../src/SAML2/AuthnRequest.php

-
message: '#^Parameter \#1 \$string of function base64_encode expects string, string\|false given\.$#'
identifier: argument.type
Expand Down Expand Up @@ -642,18 +630,6 @@ parameters:
count: 1
path: ../../src/SAML2/Extensions/Extensions.php

-
message: '#^Cannot call method saveXML\(\) on DOMDocument\|null\.$#'
identifier: method.nonObject
count: 1
path: ../../src/SAML2/Extensions/GsspUserAttributesChunk.php

-
message: '#^Method Surfnet\\SamlBundle\\SAML2\\Extensions\\GsspUserAttributesChunk\:\:toXML\(\) has no return type specified\.$#'
identifier: missingType.return
count: 1
path: ../../src/SAML2/Extensions/GsspUserAttributesChunk.php

-
message: '#^Parameter \#1 \$element of method Surfnet\\SamlBundle\\SAML2\\Extensions\\Chunk\:\:append\(\) expects DOMElement, DOMElement\|null given\.$#'
identifier: argument.type
Expand All @@ -678,12 +654,6 @@ parameters:
count: 1
path: ../../src/SAML2/ReceivedAuthnRequest.php

-
message: '#^Method Surfnet\\SamlBundle\\SAML2\\ReceivedAuthnRequest\:\:getAuthenticationContextClassRef\(\) should return string\|null but returns mixed\.$#'
identifier: return.type
count: 1
path: ../../src/SAML2/ReceivedAuthnRequest.php

-
message: '#^Method Surfnet\\SamlBundle\\SAML2\\ReceivedAuthnRequest\:\:getScopingRequesterIds\(\) return type has no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
Expand All @@ -708,12 +678,6 @@ parameters:
count: 1
path: ../../src/SAML2/ReceivedAuthnRequest.php

-
message: '#^Parameter \#1 \$array of function reset expects array\|object, mixed given\.$#'
identifier: argument.type
count: 1
path: ../../src/SAML2/ReceivedAuthnRequest.php

-
message: '#^Parameter \#1 \$xml of static method SAML2\\Message\:\:fromXML\(\) expects DOMElement, DOMNode given\.$#'
identifier: argument.type
Expand Down
17 changes: 11 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"ext-openssl": "*",
"psr/log": "^3.0",
"robrichards/xmlseclibs": "^3.1.4",
"simplesamlphp/saml2": "^4.6",
"simplesamlphp/saml2": ">=4.6 <4.20",
"symfony/dependency-injection": "^6.3|^7.0",
"symfony/framework-bundle": "^6.3|^7.0",
"symfony/security-bundle": "^6.3|^7.0",
"symfony/framework-bundle": "^6.4|^7.4",
"symfony/security-bundle": "^6.4|^7.4",
"symfony/templating": "^6.3|^7.0",
"twig/twig": "^3"
},
Expand All @@ -24,7 +24,6 @@
"require-dev": {
"ext-libxml": "*",
"ext-zlib": "*",
"irstea/phpcpd-shim": "^6.0",
"malukenho/docheader": "^1.1",
"mockery/mockery": "^1.5",
"overtrue/phplint": "*",
Expand All @@ -47,7 +46,6 @@
"@composer-validate",
"@license-headers",
"@phplint",
"@phpcpd",
"@phpcs",
"@phpmd",
"@test",
Expand All @@ -57,7 +55,6 @@
"composer-validate": "./ci/qa/validate",
"phplint": "./ci/qa/phplint",
"phpcs": "./ci/qa/phpcs",
"phpcpd": "./ci/qa/phpcpd",
"phpmd": "./ci/qa/phpmd",
"phpstan": "./ci/qa/phpstan",
"phpstan-baseline": "./ci/qa/phpstan-update-baseline",
Expand Down Expand Up @@ -85,6 +82,14 @@
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true,
"simplesamlphp/composer-xmlprovider-installer": true
},
"audit": {
"ignore": {
"PKSA-1fc7-xrz7-vw78": "GHSA-5cjr-mxj5-wmrx: DoS via XPath Transform in ds:Signature processing. No patched simplesamlphp/saml2 4.x release exists or is planned; 5.x/6.x are a ground-up rewrite without the SP/IdP response processing this bundle needs. Mitigated in application code: Surfnet\\SamlBundle\\Signing\\SignatureTransformGuard rejects any XPath Transform algorithm before the vulnerable code path is reached, called from Http/PostBinding.php::processResponse() before every signature verification. See src/Tests/Component/Signing/SignatureTransformGuardTest.php.",
"PKSA-yk3g-3g3t-ts6q": "GHSA-6929-8p9f-26jx: TLS validator confusion in HTTP-Artifact binding resolution. This bundle implements only HTTP-Redirect (Http/RedirectBinding.php) and HTTP-POST (Http/PostBinding.php) bindings; HTTP-Artifact binding is not implemented and this code path is unreachable.",
"PKSA-rxdv-j1j4-96fj": "GHSA-46r4-f8gj-xg56: HTTP-Redirect signature verification bypass, fixed in simplesamlphp/saml2 >=4.16.16. Locked version is 4.19.2 (see composer.lock), already patched.",
"PKSA-1983-c8jn-trgm": "GHSA-pxm4-r5ph-q2m2: XXE in SAML message parsing, fixed in simplesamlphp/saml2 >=4.6.14. Locked version is 4.19.2 (see composer.lock), already patched."
}
}
}
}
3 changes: 3 additions & 0 deletions src/Http/PostBinding.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Surfnet\SamlBundle\Http\Exception\UnknownServiceProviderException;
use Surfnet\SamlBundle\SAML2\AuthnRequest;
use Surfnet\SamlBundle\SAML2\ReceivedAuthnRequest;
use Surfnet\SamlBundle\Signing\SignatureTransformGuard;
use Surfnet\SamlBundle\Signing\SignatureVerifier;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -72,6 +73,8 @@ public function processResponse(

$asXml = DOMDocumentFactory::fromString($response);

SignatureTransformGuard::assertNoForbiddenTransforms($asXml);

try {
$assertions = $this->responseProcessor->process(
$serviceProvider,
Expand Down
13 changes: 13 additions & 0 deletions src/SAML2/Extensions/Extensions.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,17 @@ public function hasGsspUserAttributesChunk(): bool
{
return array_key_exists('UserAttributes', $this->chunks);
}

public function getMduiChunk(): ?MduiChunk
{
if (!$this->hasMduiChunk()) {
return null;
}
return new MduiChunk($this->chunks['UIInfo']->getValue());
}

public function hasMduiChunk(): bool
{
return array_key_exists('UIInfo', $this->chunks);
}
}
3 changes: 3 additions & 0 deletions src/SAML2/Extensions/ExtensionsMapperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ private function loadExtensionsFromSaml2AuthNRequest(): void
'UserAttributes' => $this->extensions->addChunk(
new GsspUserAttributesChunk($rawChunk->getXML())
),
'UIInfo' => $this->extensions->addChunk(
new MduiChunk($rawChunk->getXML())
),
default => $this->extensions->addChunk(
new Chunk(
$rawChunk->getLocalName(),
Expand Down
13 changes: 11 additions & 2 deletions src/SAML2/Extensions/GsspUserAttributesChunk.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use DOMDocument;
use DOMElement;
use RuntimeException;
use SAML2\Utils;

class GsspUserAttributesChunk extends Chunk
Expand Down Expand Up @@ -65,9 +66,17 @@ public function addAttribute(string $name, string $format, string $value): void
$this->append($doc->documentElement);
}

public function toXML()
public function toXML(): string
{
return $this->getValue()->ownerDocument->saveXML();
$doc = $this->getValue()->ownerDocument;
if ($doc === null) {
throw new RuntimeException('DOMElement has no ownerDocument');
}
$xml = $doc->saveXML();
if ($xml === false) {
throw new RuntimeException('Failed to serialize XML document');
}
return $xml;
}

public static function fromXML(string $xmlString): GsspUserAttributesChunk
Expand Down
90 changes: 90 additions & 0 deletions src/SAML2/Extensions/MduiChunk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* Copyright 2026 SURFnet bv
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Surfnet\SamlBundle\SAML2\Extensions;

use DOMDocument;
use DOMElement;
use RuntimeException;

class MduiChunk extends Chunk
{
private const MDUI_NAMESPACE = 'urn:oasis:names:tc:SAML:metadata:ui';
private const DISPLAY_NAME_ELEMENT = 'DisplayName';

public function __construct(?DOMElement $value = null)
{
$doc = new DOMDocument('1.0', 'UTF-8');
$root = $doc->createElementNS(self::MDUI_NAMESPACE, 'mdui:UIInfo');

if ($value && $value->hasChildNodes()) {
foreach ($value->childNodes as $child) {
$root->appendChild($doc->importNode($child->cloneNode(true), true));
}
}

$doc->appendChild($doc->importNode($root, true));
$element = $doc->documentElement;
if ($element === null) {
throw new RuntimeException('Failed to create UIInfo root element');
}
parent::__construct('UIInfo', self::MDUI_NAMESPACE, $element);
}

/**
* @return array<string, string> keyed by xml:lang
*/
public function getDisplayNames(): array
{
$names = [];
foreach ($this->getValue()->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
if ($child->localName !== self::DISPLAY_NAME_ELEMENT || $child->namespaceURI !== self::MDUI_NAMESPACE) {
continue;
}
$lang = $child->getAttribute('xml:lang');
$value = $child->textContent;
if ($lang !== '' && $value !== '') {
$names[$lang] = $value;
}
}
return $names;
}

public function toXML(): string
{
$doc = $this->getValue()->ownerDocument;
if ($doc === null) {
throw new RuntimeException('DOMElement has no ownerDocument');
}
$xml = $doc->saveXML();
if ($xml === false) {
throw new RuntimeException('Failed to serialize XML document');
}
return $xml;
}

public static function fromXML(string $xmlString): self
{
$doc = new DOMDocument();
$doc->loadXML($xmlString);
return new self($doc->documentElement);
}
}
Loading