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
257 changes: 257 additions & 0 deletions src/DataCollection/DataCollectionOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<?php

declare(strict_types=1);

namespace Sentry\DataCollection;

final class DataCollectionOptions
{
/**
* @internal
*/
public const HTTP_BODY_TYPES = [
'incomingRequest',
'outgoingRequest',
'incomingResponse',
'outgoingResponse',
];

public const SENSITIVE_DEFAULTS = [
'auth',
'token',
'secret',
'password',
'passwd',
'pwd',
'key',
'jwt',
'bearer',
'sso',
'saml',
'csrf',
'xsrf',
'credentials',
'session',
'sid',
'identity',
];

public const EXTENDED_DENY_TERMS = [
'forwarded',
'-ip',
'remote-',
'via',
'-user',
];

/**
* @var array<string, mixed>
*
* @phpstan-var array{
* user_info: bool,
* cookies: array{mode: string, terms: string[]},
* http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}},
* http_bodies: string[],
* query_params: array{mode: string, terms: string[]},
* gen_ai: array{inputs: bool, outputs: bool},
* stack_frame_variables: bool,
* frame_context_lines: int
* }
*/
private $options;

/**
* @param array<string, mixed> $options
*
* @phpstan-param array{
* user_info: bool,
* cookies: array{mode: string, terms: string[]},
* http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}},
* http_bodies: string[],
* query_params: array{mode: string, terms: string[]},
* gen_ai: array{inputs: bool, outputs: bool},
* stack_frame_variables: bool,
* frame_context_lines: int
* } $options
*/
public function __construct(array $options)
{
$this->options = $options;
}

public static function default(): self
{
return new self([
'user_info' => true,
'cookies' => self::getDefaultKeyValueCollection(),
'http_headers' => [
'request' => self::getDefaultKeyValueCollection(),
'response' => self::getDefaultKeyValueCollection(),
],
'http_bodies' => self::HTTP_BODY_TYPES,
'query_params' => self::getDefaultKeyValueCollection(),
'gen_ai' => [
'inputs' => true,
'outputs' => true,
],
'stack_frame_variables' => true,
'frame_context_lines' => 5,
]);
}

/**
* @return array{mode: string, terms: string[]}
*/
public static function getDefaultKeyValueCollection(): array
{
return [
'mode' => 'denyList',
'terms' => [],
];
}

public function shouldCollectUserInfo(): bool
{
return $this->options['user_info'];
}

public function setUserInfo(bool $userInfo): self
{
$this->options['user_info'] = $userInfo;

return $this;
}

/**
* @return array{mode: string, terms: string[]}
*/
public function getCookies(): array
{
return $this->options['cookies'];
}

/**
* @param array{mode: string, terms: string[]} $cookies
*/
public function setCookies(array $cookies): self
{
$this->options['cookies'] = $cookies;

return $this;
}

/**
* @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}
*/
public function getHttpHeaders(): array
{
return $this->options['http_headers'];
}

/**
* @param array{mode?: string, terms?: string[]}|array{request?: array{mode?: string, terms?: string[]}, response?: array{mode?: string, terms?: string[]}} $httpHeaders
*/
public function setHttpHeaders(array $httpHeaders): self
{
$this->options['http_headers'] = DataCollectionOptionsNormalizer::normalizeHttpHeaders($httpHeaders);

return $this;
}

/**
* @return string[]
*/
public function getHttpBodies(): array
{
return $this->options['http_bodies'];
}

/**
* @param string[] $httpBodies
*/
public function setHttpBodies(array $httpBodies): self
{
$this->options['http_bodies'] = $httpBodies;

return $this;
}
Comment on lines +172 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The setHttpBodies() method lacks validation, allowing invalid values to be set. This bypasses the validation logic applied during initial option configuration.
Severity: LOW

Suggested Fix

The setHttpBodies() method should validate its input to ensure it only contains allowed values, similar to how setHttpHeaders() calls normalizeHttpHeaders(). Create a normalizeHttpBodies() method that validates the input array against the HTTP_BODY_TYPES constants and call it from within setHttpBodies().

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/DataCollection/DataCollectionOptions.php#L172-L177

Potential issue: The `setHttpBodies()` method in `DataCollectionOptions` directly sets
the `http_bodies` option without any validation. This is inconsistent with the
validation logic present in `Options.php` which ensures `http_bodies` only contains
values from `HTTP_BODY_TYPES`. It is possible to bypass this initial validation by
retrieving the `DataCollectionOptions` object and calling `setHttpBodies()` directly
with an invalid array. While the immediate impact is unclear as the SDK does not yet
seem to use this value, storing invalid data violates the class's invariants and could
lead to unexpected behavior in future code that consumes it. Other setters like
`setHttpHeaders()` already perform normalization, highlighting this inconsistency.


/**
* @return array{mode: string, terms: string[]}
*/
public function getQueryParams(): array
{
return $this->options['query_params'];
}

/**
* @param array{mode: string, terms: string[]} $queryParams
*/
public function setQueryParams(array $queryParams): self
{
$this->options['query_params'] = $queryParams;

return $this;
}

/**
* @return array{inputs: bool, outputs: bool}
*/
public function getGenAi(): array
{
return $this->options['gen_ai'];
}

/**
* @param array{inputs: bool, outputs: bool} $genAi
*/
public function setGenAi(array $genAi): self
{
$this->options['gen_ai'] = $genAi;

return $this;
}

public function shouldCollectStackFrameVariables(): bool
{
return $this->options['stack_frame_variables'];
}

public function setStackFrameVariables(bool $stackFrameVariables): self
{
$this->options['stack_frame_variables'] = $stackFrameVariables;

return $this;
}

public function getFrameContextLines(): int
{
return $this->options['frame_context_lines'];
}

public function setFrameContextLines(int $frameContextLines): self
{
$this->options['frame_context_lines'] = $frameContextLines;

return $this;
}

/**
* @return array<string, mixed>
*
* @phpstan-return array{
* user_info: bool,
* cookies: array{mode: string, terms: string[]},
* http_headers: array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}},
* http_bodies: string[],
* query_params: array{mode: string, terms: string[]},
* gen_ai: array{inputs: bool, outputs: bool},
* stack_frame_variables: bool,
* frame_context_lines: int
* }
*/
public function toArray(): array
{
return $this->options;
Comment thread
Litarnus marked this conversation as resolved.
}
}
94 changes: 94 additions & 0 deletions src/DataCollection/DataCollectionOptionsNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace Sentry\DataCollection;

use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* @internal
*/
final class DataCollectionOptionsNormalizer
{
private function __construct()
{
}

/**
* @param array<string, mixed> $value
*
* @return array{mode: string, terms: string[]}
*/
public static function normalizeKeyValueCollection(array $value): array
{
$resolver = new OptionsResolver();
$resolver->setDefaults(DataCollectionOptions::getDefaultKeyValueCollection());
$resolver->setAllowedTypes('mode', 'string');
$resolver->setAllowedTypes('terms', 'string[]');
$resolver->setAllowedValues('mode', [
'off',
'denyList',
'allowList',
]);

/** @var array{mode: string, terms: string[]} $resolvedOptions */
$resolvedOptions = $resolver->resolve($value);

return $resolvedOptions;
}

/**
* @param array<string, mixed> $value
*
* @return array{request: array{mode: string, terms: string[]}, response: array{mode: string, terms: string[]}}
*/
public static function normalizeHttpHeaders(array $value): array
{
if (!isset($value['request']) && !isset($value['response'])) {
$headers = self::normalizeKeyValueCollection($value);

return [
'request' => $headers,
'response' => $headers,
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shared request response header arrays

Medium Severity

When flat http_headers config is normalized, request and response are assigned the same array instance. Later in-place changes to one direction’s mode or terms also change the other, unlike DataCollectionOptions::default() and per-direction config.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee33464. Configure here.

}

$resolver = new OptionsResolver();
$resolver->setDefaults([
'request' => [],
'response' => [],
]);
$resolver->setAllowedTypes('request', 'array');
$resolver->setAllowedTypes('response', 'array');

/** @var array{request: array<string, mixed>, response: array<string, mixed>} $resolvedOptions */
$resolvedOptions = $resolver->resolve($value);

return [
'request' => self::normalizeKeyValueCollection($resolvedOptions['request']),
'response' => self::normalizeKeyValueCollection($resolvedOptions['response']),
];
}

/**
* @param array<string, mixed> $value
*
* @return array{inputs: bool, outputs: bool}
*/
public static function normalizeGenAi(array $value): array
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'inputs' => true,
'outputs' => true,
]);
$resolver->setAllowedTypes('inputs', 'bool');
$resolver->setAllowedTypes('outputs', 'bool');

/** @var array{inputs: bool, outputs: bool} $resolvedOptions */
$resolvedOptions = $resolver->resolve($value);

return $resolvedOptions;
}
}
Loading