-
Notifications
You must be signed in to change notification settings - Fork 125
Add sniff to detect direct access to AI Connector API keys and give error #1362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ishitaj34
wants to merge
6
commits into
WordPress:trunk
Choose a base branch
from
ishitaj34:fix/issue-1342
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bbb0659
add: AI connector API key access sniff
ishitaj34 cb06c1c
Merge branch 'trunk' into fix/issue-1342
davidperezgar 7564427
add: PHP 8 named argument support and tests
ishitaj34 9edca71
Merge branch 'trunk' into fix/issue-1342
davidperezgar 772d73a
docs: add @since tags
ishitaj34 2870b53
docs: add @since tags to test file
ishitaj34 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
173 changes: 173 additions & 0 deletions
173
phpcs-sniffs/PluginCheck/Sniffs/Security/AIConnectorAPIKeySniff.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| <?php | ||
| /** | ||
| * AIConnectorAPIKeySniff | ||
| * | ||
| * Detects direct access to WordPress AI Connector API keys. | ||
| * | ||
| * @package PluginCheck | ||
| */ | ||
|
|
||
| namespace PluginCheckCS\PluginCheck\Sniffs\Security; | ||
|
|
||
| use PHPCSUtils\Utils\PassedParameters; | ||
| use PHPCSUtils\Utils\TextStrings; | ||
| use WordPressCS\WordPress\AbstractFunctionParameterSniff; | ||
|
|
||
| /** | ||
| * Detect direct access to WordPress AI Connector API keys. | ||
| * | ||
| * Plugins should use the WordPress AI Connector APIs rather than | ||
| * reading provider API keys directly from the options table. | ||
| * | ||
| * @since 2.2.0 | ||
| */ | ||
| final class AIConnectorAPIKeySniff extends AbstractFunctionParameterSniff { | ||
|
|
||
| /** | ||
| * The group name for this group of functions. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $group_name = 'ai_connector_api_key'; | ||
|
|
||
| /** | ||
| * List of functions to examine. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @var array<string, true> | ||
| */ | ||
| protected $target_functions = array( | ||
| 'get_option' => true, | ||
| 'get_site_option' => true, | ||
| 'get_network_option' => true, | ||
| 'get_options' => true, | ||
| ); | ||
|
|
||
| /** | ||
| * Parameter positions for supported functions. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @var array<string, int> | ||
| */ | ||
| private $param_positions = array( | ||
| 'get_option' => 1, | ||
| 'get_site_option' => 1, | ||
| 'get_network_option' => 2, | ||
| 'get_options' => 1, | ||
| ); | ||
|
|
||
| /** | ||
| * Parameter names for supported functions. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @var array<string, string> | ||
| */ | ||
| private $param_names = array( | ||
| 'get_option' => 'option', | ||
| 'get_site_option' => 'option', | ||
| 'get_network_option' => 'option', | ||
| 'get_options' => 'options', | ||
| ); | ||
|
|
||
| /** | ||
| * Process the parameters of a matched function. | ||
| * | ||
| * Checks whether the requested option name matches the | ||
| * WordPress AI Connector API key naming pattern. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @param int $stackPtr The position of the current token in the stack. | ||
| * @param string $group_name The name of the group which was matched. | ||
| * @param string $matched_content The token content (function name) which was matched in lowercase. | ||
| * @param array $parameters Array with information about the parameters. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { | ||
|
|
||
| $param_position = isset( $this->param_positions[ $matched_content ] ) | ||
| ? $this->param_positions[ $matched_content ] | ||
| : 1; | ||
|
|
||
| $param_name = isset( $this->param_names[ $matched_content ] ) | ||
| ? $this->param_names[ $matched_content ] | ||
| : 'option'; | ||
|
|
||
| $option_param = PassedParameters::getParameterFromStack( | ||
| $parameters, | ||
| $param_position, | ||
| $param_name | ||
| ); | ||
|
|
||
| if ( false === $option_param ) { | ||
| return; | ||
| } | ||
|
|
||
| // Handle get_options(). | ||
| if ( 'get_options' === $matched_content ) { | ||
|
|
||
| // Extract option names from the get_options() array parameter. | ||
| preg_match_all( | ||
| '/["\']([^"\']+)["\']/', | ||
| $option_param['clean'], | ||
| $matches | ||
| ); | ||
|
|
||
| foreach ( $matches[1] as $option_name ) { | ||
| if ( $this->is_connector_api_key( $option_name ) ) { | ||
| $this->add_error( $stackPtr ); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $option_name = TextStrings::stripQuotes( $option_param['clean'] ); | ||
|
|
||
| if ( ! $this->is_connector_api_key( $option_name ) ) { | ||
| return; | ||
| } | ||
|
|
||
| $this->add_error( $stackPtr ); | ||
| } | ||
|
|
||
| /** | ||
| * Adds an error for direct AI Connector API key access. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @param int $stackPtr Position of the function call. | ||
| * | ||
| * @return void | ||
| */ | ||
| private function add_error( $stackPtr ) { | ||
|
|
||
| $this->phpcsFile->addError( | ||
| 'Your plugin reads WordPress AI Connector API keys directly from the options table. Plugins should not access connector credentials directly. Use the WordPress AI Client instead.', | ||
| $stackPtr, | ||
| 'DirectAIConnectorAPIKeyAccess' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether an option name matches the AI Connector API key pattern. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @param string $option_name Option name. | ||
| * @return bool True when the option name matches the AI Connector API key pattern. | ||
| */ | ||
| private function is_connector_api_key( $option_name ) { | ||
| return (bool) preg_match( | ||
| '/^connectors_ai_[a-z0-9_]+_api_key$/i', | ||
| $option_name | ||
| ); | ||
| } | ||
| } | ||
31 changes: 31 additions & 0 deletions
31
phpcs-sniffs/PluginCheck/Tests/Security/AIConnectorAPIKeyUnitTest.inc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| get_option( 'connectors_ai_openai_api_key' ); // Error. | ||
| get_site_option( 'connectors_ai_anthropic_api_key' ); // Error. | ||
| get_network_option( null, 'connectors_ai_grok_api_key' ); // Error. | ||
|
|
||
| get_options( | ||
| array( | ||
| 'connectors_ai_openai_api_key', | ||
| 'connectors_ai_custom_provider_api_key', | ||
| ) | ||
| ); // Error. | ||
|
|
||
| get_options( | ||
| array( | ||
| "connectors_ai_openai_api_key", | ||
| ) | ||
| ); // Error. | ||
|
|
||
| get_option( 'siteurl' ); // Good. | ||
| get_site_option( 'blogname' ); // Good. | ||
| get_network_option( null, 'admin_email' ); // Good. | ||
|
|
||
| get_option( option: 'connectors_ai_openai_api_key' ); // Error. | ||
| get_site_option( option: 'connectors_ai_anthropic_api_key' ); // Error. | ||
| get_network_option( network_id: null, option: 'connectors_ai_grok_api_key' ); // Error. | ||
| get_options( options: array( 'connectors_ai_openai_api_key' ) ); // Error. | ||
|
|
||
| get_option( option: 'siteurl' ); // Good. | ||
| get_site_option( option: 'blogname' ); // Good. | ||
| get_network_option( network_id: null, option: 'admin_email' ); // Good. |
73 changes: 73 additions & 0 deletions
73
phpcs-sniffs/PluginCheck/Tests/Security/AIConnectorAPIKeyUnitTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| <?php | ||
| /** | ||
| * Unit tests for AIConnectorAPIKeySniff. | ||
| * | ||
| * @package PluginCheck | ||
| */ | ||
|
|
||
| namespace PluginCheckCS\PluginCheck\Tests\Security; | ||
|
|
||
| use PHP_CodeSniffer\Sniffs\Sniff; | ||
| use PluginCheckCS\PluginCheck\Sniffs\Security\AIConnectorAPIKeySniff; | ||
| use PluginCheckCS\PluginCheck\Tests\AbstractSniffUnitTest; | ||
|
|
||
| /** | ||
| * Unit tests for AIConnectorAPIKeySniff. | ||
| * | ||
| * @since 2.2.0 | ||
| */ | ||
| final class AIConnectorAPIKeyUnitTest extends AbstractSniffUnitTest { | ||
|
|
||
| /** | ||
| * Returns the lines where errors should occur. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @return array <int line number> => <int number of errors> | ||
| */ | ||
| public function getErrorList() { | ||
| return array( | ||
| 3 => 1, // get_option() reading Connector API key. | ||
| 4 => 1, // get_site_option() reading Connector API key. | ||
| 5 => 1, // get_network_option() reading Connector API key. | ||
| 7 => 1, // get_options() reading Connector API key (single quotes). | ||
| 14 => 1, // get_options() reading Connector API key (double quotes). | ||
| 24 => 1, // get_option() with named arg. | ||
| 25 => 1, // get_site_option() with named arg. | ||
| 26 => 1, // get_network_option() with named arg. | ||
| 27 => 1, // get_options() with named arg. | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the lines where warnings should occur. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @return array <int line number> => <int number of warnings> | ||
| */ | ||
| public function getWarningList() { | ||
| return array(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the fully qualified class name (FQCN) of the sniff. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @return string | ||
| */ | ||
| protected function get_sniff_fqcn() { | ||
| return AIConnectorAPIKeySniff::class; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the parameters for the sniff. | ||
| * | ||
| * @since 2.2.0 | ||
| * | ||
| * @param Sniff $sniff The sniff being tested. | ||
| */ | ||
| public function set_sniff_parameters( Sniff $sniff ) { | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.