Skip to content

LW-3297 Harden parent correlation ID capture - #25

Merged
mSprunskas merged 1 commit into
paysera:masterfrom
siarhei-kruk:LW-3297-validate-parent-correlation-id
Jul 8, 2026
Merged

LW-3297 Harden parent correlation ID capture#25
mSprunskas merged 1 commit into
paysera:masterfrom
siarhei-kruk:LW-3297-validate-parent-correlation-id

Conversation

@siarhei-kruk

@siarhei-kruk siarhei-kruk commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes capture of the client-controlled Paysera-Correlation-Id header safe: the parent ID is validated in the provider (the single place the invariant is enforced for every caller) and reset at the start of every main HTTP request so it can't leak across requests in a reused process. Log output for legitimate IDs is unchanged.

Why

This bundle emits the incoming header as parent_corr_id to link cross-service traces. The value is client-controlled but was captured verbatim with no bounds:

  • an empty header produced an empty parent_corr_id (no tracing value);
  • an arbitrarily long or hostile value could inject control characters / whitespace / structural punctuation into every log line and Sentry tag (log injection, bloat);
  • on the HTTP path the captured value was never reset at request start, so in a long-running/reused process (e.g. RoadRunner) a parent ID from a previous request could leak into a later request carrying no header.

What changed

ParentCorrelationIdProvider — single owner of the validation rule

  • setParentCorrelationId() validates and silently ignores an invalid value (empty, longer than MAX_LENGTH = 128, or outside PATTERN = /^[A-Za-z0-9._-]+\z/). An existing valid value is not clobbered by a subsequent invalid one.
  • 128 stays under Sentry's 200-char tag limit; the charset is the alphabet the bundle's own correlation ID generator emits. \z (not $) anchors strictly so a trailing newline cannot slip through.

ParentCorrelationIdListener — pure consumer

  • Resets the provider at the start of every main request, before reading the header (the worker path was already reset via IterationEndListener; this closes the HTTP path).
  • Reads the header and hands it to the provider; validation is the provider's responsibility.

Design notes

  • Validation lives in the provider, silently dropping invalid input rather than throwing. A correlation ID is best-effort tracing metadata that must never break a request or worker, so ignoring a malformed value is the correct contract for every caller — and it keeps the listener trivial with no exception handling on the request path.
  • BC. setParentCorrelationId() now ignores values it previously stored verbatim (empty / out-of-charset / >128 chars); observable log output for legitimate IDs is identical.

Tests / Test plan

  • composer install
  • vendor/bin/phpunit47 tests, 1191 assertions, green.

Coverage added:

  • Provider: max-length (128) accepted; invalid values (empty, 129 chars, space, newline injection, tab, JSON punctuation, slash) ignored and do not clobber an existing valid value.
  • Listener: valid capture, max-length accept, stale-value reset on absent/empty/too-long/invalid-charset header, valid-header overwrite of a stale value, sub-requests still ignored.

Reviewer check: capture behavior for legitimate IDs is unchanged; only empty/over-length/out-of-charset values are dropped, and the HTTP path no longer leaks a stale parent ID across requests.

Housekeeping

  • CHANGELOG.md: ## 3.3.2### Security (provider-enforced validation) and ### Fixed (request-start reset).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces validation and sanitization for the parent correlation ID to prevent potential log injection and bloat. It enforces a maximum length of 128 characters and restricts allowed characters to an alphanumeric set including dots, underscores, and hyphens. Additionally, it ensures the parent correlation ID is reset at the start of each main HTTP request to prevent data leakage across reused processes. Comprehensive unit tests have been added to verify these changes. I have no further feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@siarhei-kruk
siarhei-kruk force-pushed the LW-3297-validate-parent-correlation-id branch 2 times, most recently from e06a166 to 74f1956 Compare July 7, 2026 11:53

@mSprunskas mSprunskas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good in general, please remove all redundant comments though, thanks.

class ParentCorrelationIdProvider
{
// Stays under Sentry's 200-char tag limit.
private const MAX_LENGTH = 128;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not make it 200 then? If length is also indication of who generated the value (us or someone else), then Sentry is irrelevant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

128 is more than enough. Mention of sentry is removed.


private ?string $parentCorrelationId;
// The alphabet the bundle's own correlation id generator emits; `\z` (not `$`)
// anchors strictly so a trailing newline cannot slip through.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comments like this should be part of PR, not part of code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

@siarhei-kruk
siarhei-kruk force-pushed the LW-3297-validate-parent-correlation-id branch from 74f1956 to 937ae6f Compare July 7, 2026 16:01

@mSprunskas mSprunskas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. Redundant empty-string check (low / simplification)
  src/Service/ParentCorrelationIdProvider.php:36
  return $parentCorrelationId !== ''
      && strlen($parentCorrelationId) <= self::MAX_LENGTH
      && preg_match(self::PATTERN, $parentCorrelationId) === 1;
  The !== '' term is dead: the pattern's + quantifier already rejects the empty string (verified: preg_match('/^[A-Za-z0-9._-]+\z/', '') returns 0). It's harmless and arguably documents intent, but it is strictly redundant. Not a correctness bug.

Comment on lines -10 to -15
private ?string $parentCorrelationId;
private const PATTERN = '/^[A-Za-z0-9._-]+\z/';

public function __construct()
{
$this->parentCorrelationId = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This was actually correct and according to our code style https://github.com/paysera/php-style-guide#default-property-values

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

reverted this change

Make capture of the client-controlled Paysera-Correlation-Id header safe:
validation is enforced in the provider (the single place the invariant is
enforced for every caller) and the parent id is reset at the start of every
main HTTP request so it cannot leak across requests in a reused process.
Log output for legitimate ids is unchanged.

- ParentCorrelationIdProvider::setParentCorrelationId() validates and
  silently ignores an invalid value (empty, over 128 chars, or outside
  [A-Za-z0-9._-]); an existing valid value is not clobbered by a later
  invalid one. `\z` anchors the pattern so a trailing newline cannot slip
  through.
- ParentCorrelationIdListener resets the provider at the start of every
  main request before reading the header, then hands the header to the
  provider; validation is the provider's responsibility.
@siarhei-kruk
siarhei-kruk force-pushed the LW-3297-validate-parent-correlation-id branch from 937ae6f to 7636664 Compare July 8, 2026 08:01
@mSprunskas
mSprunskas merged commit d350954 into paysera:master Jul 8, 2026
38 checks passed
@siarhei-kruk
siarhei-kruk deleted the LW-3297-validate-parent-correlation-id branch July 8, 2026 09:09
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