Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ body:
id: versions
attributes:
label: Versions
placeholder: 'Procore Connect 2.0.0, WordPress 6.9, PHP 8.2'
placeholder: 'Procore Connect 2.0.3, WordPress 7.0, PHP 8.3'
validations:
required: true

Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to Procore Connect are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.4] — 2026-08-08

### Security

- `SECURITY.md` no longer recommends a version that destroys credentials. It listed all
of 2.0.x as supported and told readers to "upgrade to 2.0.0" — but 2.0.0 and 2.0.1 are
precisely the versions that over-encrypt the stored client secret until it is
unreadable. Both are now marked unsupported, with the fault, its blast radius and the
remedy stated plainly. The 1.x plaintext-storage warning is separated out and keeps its
instruction to rotate credentials.

### Added

- Tests for `Support\Logger`: redaction of secrets from log context, including nested
arrays, plus truncation and the bounded ring buffer. This is what stands between a
debug session and a client secret in `debug.log`, and it had no coverage.
- Tests for `Frontend\Renderer`: the child-theme → parent-theme → plugin override chain,
that an override actually renders and receives its data, directory-traversal refusal,
and notice escaping. Copying a template into a theme is a documented headline feature
and it had no coverage.

### Changed

- Refreshed the version placeholder in the bug-report template.

## [2.0.3] — 2026-08-08

### Fixed
Expand Down Expand Up @@ -206,6 +231,7 @@ every install was non-functional regardless of configuration.

- Initial release.

[2.0.4]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.4
[2.0.3]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.3
[2.0.2]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.2
[2.0.1]: https://github.com/ibuilder/ProcoreWP/releases/tag/v2.0.1
Expand Down
40 changes: 30 additions & 10 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,36 @@

## Supported versions

| Version | Supported |
|---------|-----------|
| 2.0.x | ✅ |
| 1.x | ❌ — see the note below |

Version 1.x is not supported and should not be used. It stored the Procore client secret
and access token as plaintext in an autoloaded WordPress option, ran its connection test
from an unverified `$_POST` key, and registered its settings with no sanitize callback.
It also could not authenticate against the live API. Upgrade to 2.0.0 and rotate any
credentials that were stored by 1.x, on the assumption they were exposed.
| Version | Supported | Notes |
|---------|-----------|-------|
| 2.0.3 | ✅ | Current |
| 2.0.2 | ⚠️ | Safe, but does not repair a credential already damaged by 2.0.0 or 2.0.1 |
| 2.0.0 – 2.0.1 | ❌ | **Destroy the stored client secret.** See below |
| 1.x | ❌ | Stores credentials in plaintext. See below |

### 2.0.0 and 2.0.1 — stored credential destroyed

These versions attached the settings sanitizer to `sanitize_option_{$option}` and
encrypted the client secret unconditionally. WordPress runs that filter on *every*
`update_option()` for the option, including the plugin's own internal writes, so the
stored secret gained an encryption layer on each write until it could no longer be
decrypted. The site then failed to authenticate with nothing on screen to explain why.

This is a data-integrity fault rather than a disclosure: the value was over-encrypted,
never exposed. Upgrade to **2.0.3**, which unwraps and restores the value automatically
and reports what it did. If it cannot be recovered the secret is cleared and you are
asked to re-enter it.

### 1.x — credentials stored in plaintext

Version 1.x stored the Procore client secret, access token and refresh token as plaintext
in an **autoloaded** WordPress option, read into memory on every page request and present
in every database backup. It also ran its connection test from an unverified `$_POST` key
and registered its settings with no sanitize callback, and it could not authenticate
against the live API at all.

Upgrade to 2.0.3 and **rotate any credentials 1.x stored**, on the assumption they were
exposed.

## Reporting a vulnerability

Expand Down
7 changes: 6 additions & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,13 @@
<rule ref="Generic.Commenting">
<exclude-pattern>/tests/wp-shims.php</exclude-pattern>
</rule>
<!--
Tests build real fixture files on disk. WP_Filesystem is a runtime
abstraction that does not exist in the shimmed harness, so the direct
calls are correct here.
-->
<rule ref="WordPress.WP.AlternativeFunctions">
<exclude-pattern>/tests/wp-shims.php</exclude-pattern>
<exclude-pattern>/tests/*</exclude-pattern>
</rule>
<rule ref="WordPress.PHP.DiscouragedPHPFunctions">
<exclude-pattern>/tests/wp-shims.php</exclude-pattern>
Expand Down
4 changes: 2 additions & 2 deletions procore-connect.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Procore Connect
* Plugin URI: https://github.com/ibuilder/ProcoreWP
* Description: Connect WordPress to the Procore construction management platform. Display projects, teams, drawings, RFIs and more with shortcodes, blocks and a cached REST proxy.
* Version: 2.0.3
* Version: 2.0.4
* Requires at least: 6.5
* Requires PHP: 7.4
* Author: ibuilder
Expand All @@ -22,7 +22,7 @@

defined( 'ABSPATH' ) || exit;

const VERSION = '2.0.3';
const VERSION = '2.0.4';

define( 'PROCORE_CONNECT_VERSION', VERSION );
define( 'PROCORE_CONNECT_FILE', __FILE__ );
Expand Down
10 changes: 9 additions & 1 deletion readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tags: procore, construction, project management, shortcode, api
Requires at least: 6.5
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 2.0.3
Stable tag: 2.0.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -127,6 +127,11 @@ Yes. Caching goes through the transient API, so a persistent object cache such a

== Changelog ==

= 2.0.4 =

* Security documentation no longer recommends 2.0.0 or 2.0.1, the versions that damage the stored Client Secret. Both are now marked unsupported with the remedy stated plainly.
* Added test coverage for log redaction, which keeps secrets out of debug.log, and for the theme template override chain.

= 2.0.3 =

* **Repairs credentials damaged by 2.0.0 or 2.0.1.** Those versions re-encrypted the stored Client Secret on every settings save until it could no longer be read, leaving the site unable to authenticate with no explanation. 2.0.2 stopped the damage; 2.0.3 undoes it. The upgrade unwraps the value, re-stores it correctly, and tells you what it did. If it cannot be recovered it is cleared and you are asked to enter it again.
Expand Down Expand Up @@ -178,6 +183,9 @@ A complete rewrite. See the upgrade notice below before updating.

== Upgrade Notice ==

= 2.0.4 =
Documentation and test coverage only. No functional change from 2.0.3.

= 2.0.3 =
Repairs a Client Secret damaged by 2.0.0 or 2.0.1, which re-encrypted it on every save until it stopped working. Upgrading restores it automatically and reports the result. Recommended for anyone who ran 2.0.0 or 2.0.1.

Expand Down
152 changes: 152 additions & 0 deletions tests/unit/LoggerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php
/**
* Diagnostic logging, and the redaction that keeps secrets out of it.
*
* @package ProcoreConnect
*/

declare( strict_types = 1 );

namespace ProcoreConnect\Tests\unit;

use ProcoreConnect\Admin\Settings;
use ProcoreConnect\Support\Logger;

/**
* The logger is the one place the plugin deliberately writes API context to
* disk, so its redaction is what stands between a debug session and a client
* secret in `debug.log`. It is worth asserting on directly.
*/
final class LoggerTest extends TestCase {

/**
* Turn logging on before each test.
*
* @return void
*/
protected function setUp(): void {
parent::setUp();

Settings::set( 'enable_logging', true );
}

/**
* Nothing is recorded while logging is switched off.
*
* @return void
*/
public function test_logging_is_off_by_default(): void {
Settings::set( 'enable_logging', false );

Logger::error( 'should not be recorded' );

$this->assertSame( array(), Logger::entries() );
}

/**
* An enabled logger records the message and its level.
*
* @return void
*/
public function test_records_message_and_level(): void {
Logger::warning( 'throttled' );

$entries = Logger::entries();

$this->assertCount( 1, $entries );
$this->assertSame( 'warning', $entries[0]['level'] );
$this->assertSame( 'throttled', $entries[0]['message'] );
}

/**
* Credentials passed as context must never be written.
*
* @return void
*/
public function test_redacts_credentials_from_context(): void {
Logger::error(
'auth failed',
array(
'client_secret' => 'sk_live_should_never_appear',
'access_token' => 'tok_should_never_appear',
'refresh_token' => 'ref_should_never_appear',
'code' => 'authcode_should_never_appear',
'password' => 'pw_should_never_appear',
'status' => 401,
)
);

$dump = (string) wp_json_encode( Logger::entries() );

foreach ( array( 'sk_live_should_never_appear', 'tok_should_never_appear', 'ref_should_never_appear', 'authcode_should_never_appear', 'pw_should_never_appear' ) as $secret ) {
$this->assertStringNotContainsString( $secret, $dump );
}

// Non-sensitive context is still useful and must survive.
$this->assertStringContainsString( '401', $dump );
}

/**
* Redaction must reach credentials nested inside arrays.
*
* @return void
*/
public function test_redacts_nested_credentials(): void {
Logger::error(
'request failed',
array(
'request' => array(
'headers' => array( 'Authorization' => 'Bearer leaked_token_value' ),
'body' => array( 'client_secret' => 'nested_secret_value' ),
),
)
);

$dump = (string) wp_json_encode( Logger::entries() );

$this->assertStringNotContainsString( 'leaked_token_value', $dump );
$this->assertStringNotContainsString( 'nested_secret_value', $dump );
$this->assertStringContainsString( '[redacted]', $dump );
}

/**
* Long context values are truncated so the log cannot balloon.
*
* @return void
*/
public function test_truncates_very_long_context_values(): void {
Logger::info( 'big', array( 'body' => str_repeat( 'x', 5000 ) ) );

$entries = Logger::entries();

$this->assertLessThanOrEqual( 500, strlen( (string) $entries[0]['context']['body'] ) );
}

/**
* The ring buffer must stay bounded, newest first.
*
* @return void
*/
public function test_ring_buffer_is_bounded_and_newest_first(): void {
for ( $i = 0; $i < 60; $i++ ) {
Logger::info( 'entry ' . $i );
}

$entries = Logger::entries();

$this->assertCount( 50, $entries );
$this->assertSame( 'entry 59', $entries[0]['message'] );
}

/**
* Clearing must empty the buffer.
*
* @return void
*/
public function test_clear_empties_the_buffer(): void {
Logger::info( 'something' );
Logger::clear();

$this->assertSame( array(), Logger::entries() );
}
}
Loading
Loading