Skip to content

Commit f034748

Browse files
authored
Log warning for SAMLRequests with IssueInstant older than 24 hours (#2061)
Why is this change needed? Prior to this change, EngineBlock only logged a generic clock-skew notice when an incoming message's IssueInstant differed from server time by more than 30 seconds, in either direction, for both AuthnRequests and Responses. There was no way to distinguish an SP sending severely stale AuthnRequests (>24h old, possibly indicating replay or a badly out-of-sync clock) from routine minor clock drift. How does it address the issue? This change adds a dedicated warning for severely stale AuthnRequests, taking priority over the existing minor-drift notice so only the single most-applicable message fires per request. The check is scoped to AuthnRequests only, since Responses are handled differently and weren't part of the reported issue. This is logging only, no requests are blocked. #1972
1 parent ddb56cc commit f034748

2 files changed

Lines changed: 68 additions & 2 deletions

File tree

library/EngineBlock/Corto/Module/Bindings.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class EngineBlock_Corto_Module_Bindings extends EngineBlock_Corto_Module_Abstrac
4949

5050
const SAML_STATUS_MESSAGE_EMPTY = '(No message provided)';
5151

52+
const OLD_ISSUE_INSTANT_THRESHOLD_SECONDS = 86400; // 24 hours
53+
5254
protected static $ASSERTION_SEQUENCE = array(
5355
'saml:Issuer',
5456
'ds:Signature',
@@ -598,10 +600,22 @@ protected function _verifyKnownIdP($messageIssuer, $destination = '')
598600
*/
599601
protected function _checkIssueInstant($issueInstant, $type, $entityid)
600602
{
601-
// check the IssueInstant against our own time to see if the SP's clock is getting out of sync
602-
// Ssp has a hard-coded limit of 60 seconds; use 30 here to catch an IdP's drifting clock early
603603
$time = EngineBlock_ApplicationSingleton::getInstance()->getDiContainer()->getTimeProvider()->time();
604604
$timeDelta = $time - $issueInstant;
605+
606+
if ($type === 'SP' && $timeDelta > self::OLD_ISSUE_INSTANT_THRESHOLD_SECONDS) {
607+
$this->_logger->warning(
608+
sprintf(
609+
'IssueInstant of SAMLRequest from SP "%s" is %d seconds (more than 24 hours) in the past; possible replay or severely out-of-sync SP clock',
610+
$entityid,
611+
$timeDelta
612+
)
613+
);
614+
return;
615+
}
616+
617+
// check the IssueInstant against our own time to see if the SP's clock is getting out of sync
618+
// Ssp has a hard-coded limit of 60 seconds; use 30 here to catch an IdP's drifting clock early
605619
if (abs($timeDelta) > 30) {
606620
$this->_logger->notice(
607621
sprintf(

tests/library/EngineBlock/Test/Corto/Module/BindingsTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,56 @@ public function testAzureDomainHintWhrParamIsCorrectlyUrlEncoded()
238238
$this->assertArrayHasKey('whr', $params, 'whr query parameter must be present');
239239
$this->assertSame('example.nl', $params['whr']);
240240
}
241+
242+
public function testOldSpIssueInstantLogsWarningOnly()
243+
{
244+
$logger = $this->mockLogger();
245+
$logger->shouldReceive('warning')->once()->with(m::pattern('/is \d+ seconds \(more than 24 hours\) in the past/'));
246+
$logger->shouldNotReceive('notice');
247+
248+
$this->checkIssueInstant(time() - 90000, 'SP', 'https://sp.example.edu');
249+
}
250+
251+
public function testRecentSpIssueInstantLogsNoticeOnly()
252+
{
253+
$logger = $this->mockLogger();
254+
$logger->shouldReceive('notice')->once()->with(m::pattern('/clock synchronization issues/'));
255+
$logger->shouldNotReceive('warning');
256+
257+
$this->checkIssueInstant(time() - 600, 'SP', 'https://sp.example.edu');
258+
}
259+
260+
public function testOldIdpIssueInstantLogsGenericNoticeNotWarning()
261+
{
262+
$logger = $this->mockLogger();
263+
$logger->shouldReceive('notice')->once()->with(m::pattern('/clock synchronization issues/'));
264+
$logger->shouldNotReceive('warning');
265+
266+
$this->checkIssueInstant(time() - 90000, 'IdP', 'https://idp.example.edu');
267+
}
268+
269+
public function testFreshSpIssueInstantLogsNothing()
270+
{
271+
$logger = $this->mockLogger();
272+
$logger->shouldNotReceive('warning');
273+
$logger->shouldNotReceive('notice');
274+
275+
$this->checkIssueInstant(time(), 'SP', 'https://sp.example.edu');
276+
}
277+
278+
private function mockLogger()
279+
{
280+
$logger = m::mock();
281+
282+
$loggerProperty = new ReflectionProperty(EngineBlock_Corto_Module_Bindings::class, '_logger');
283+
$loggerProperty->setValue($this->bindings, $logger);
284+
285+
return $logger;
286+
}
287+
288+
private function checkIssueInstant($issueInstant, $type, $entityId)
289+
{
290+
$method = new ReflectionMethod(EngineBlock_Corto_Module_Bindings::class, '_checkIssueInstant');
291+
$method->invoke($this->bindings, $issueInstant, $type, $entityId);
292+
}
241293
}

0 commit comments

Comments
 (0)