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
2 changes: 1 addition & 1 deletion accessibility-checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

// Current database version.
if ( ! defined( 'EDAC_DB_VERSION' ) ) {
define( 'EDAC_DB_VERSION', '1.0.6' );
define( 'EDAC_DB_VERSION', '1.0.7' );
}

// Plugin Folder Path.
Expand Down
6 changes: 4 additions & 2 deletions admin/class-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ function ( $a, $b ) {

foreach ( $rules as $rule ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for interacting with custom database, safe variable used for table name, caching not required for one time operation.
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment, ignre_reason, ignre_global, landmark, landmark_selector FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A );
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, postid, object, ruletype, ignre, ignre_user, ignre_date, ignre_comment, ignre_reason, ignre_global, landmark, landmark_selector, extra_data FROM %i where postid = %d and rule = %s and siteid = %d', $table_name, $postid, $rule['slug'], $siteid ), ARRAY_A );
$count_classes = ( 'error' === $rule['rule_type'] ) ? ' edac-details-rule-count-error' : ' edac-details-rule-count-warning';
$count_classes .= ( 0 !== $rule['count'] ) ? ' active' : '';

Expand Down Expand Up @@ -507,7 +507,9 @@ function ( $a, $b ) {
$id
) . '</h4>';

$html .= '<div id="edac-details-rule-records-record-' . $id . '" class="edac-details-rule-records-record">';
$extra_data_attr = ! empty( $row['extra_data'] ) ? ' data-extra-data="' . esc_attr( $row['extra_data'] ) . '"' : '';

$html .= '<div id="edac-details-rule-records-record-' . $id . '" class="edac-details-rule-records-record"' . $extra_data_attr . '>';

$html .= '<div class="edac-details-rule-records-record-cell edac-details-rule-records-record-object">';

Expand Down
3 changes: 2 additions & 1 deletion admin/class-frontend-highlight.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function get_issues( $post_id ) {
$table_name = $wpdb->prefix . 'accessibility_checker';
$post_id = (int) $post_id;
$siteid = get_current_blog_id();
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, rule, ignre, object, ruletype, selector, ancestry, xpath FROM %i where postid = %d and siteid = %d', $table_name, $post_id, $siteid ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name.
$results = $wpdb->get_results( $wpdb->prepare( 'SELECT id, rule, ignre, object, ruletype, selector, ancestry, xpath, extra_data FROM %i where postid = %d and siteid = %d', $table_name, $post_id, $siteid ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Safe variable used for table name.
if ( ! $results ) {
return null;
}
Expand Down Expand Up @@ -143,6 +143,7 @@ public function ajax() {
$array['selector'] = $result['selector'] ?? '';
$array['ancestry'] = $result['ancestry'] ?? '';
$array['xpath'] = $result['xpath'] ?? '';
$array['extra_data'] = ! empty( $result['extra_data'] ) ? json_decode( $result['extra_data'], true ) : null;

Comment on lines +146 to 147

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

extra_data is decoded from JSON without checking for decode errors. If the DB contains malformed JSON, this will quietly set extra_data to null and can make frontend behavior inconsistent. Consider verifying json_last_error() (or using a helper) and only returning decoded data when valid.

Suggested change
$array['extra_data'] = ! empty( $result['extra_data'] ) ? json_decode( $result['extra_data'], true ) : null;
if ( ! empty( $result['extra_data'] ) ) {
$decoded_extra_data = json_decode( $result['extra_data'], true );
if ( JSON_ERROR_NONE === json_last_error() ) {
$array['extra_data'] = $decoded_extra_data;
} else {
$array['extra_data'] = null;
}
} else {
$array['extra_data'] = null;
}

Copilot uses AI. Check for mistakes.
$issues[] = $array;

Expand Down
8 changes: 6 additions & 2 deletions admin/class-insert-rule-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ class Insert_Rule_Data {
* @param string|null $landmark The landmark type (main, header, footer, nav), optional.
* @param string|null $landmark_selector The landmark selector, optional.
* @param array $selectors An array of selectors that point to the object, optional.
* @param array|null $extra_data Optional extra data to store as JSON (e.g. color contrast values).
*
* @return void|int|\WP_Error The ID of the inserted record, void if no
* record was inserted or a WP_Error if the insert failed.
*/
public function insert( object $post, string $rule, string $ruletype, string $rule_obj, ?string $landmark = null, ?string $landmark_selector = null, array $selectors = [] ) {
public function insert( object $post, string $rule, string $ruletype, string $rule_obj, ?string $landmark = null, ?string $landmark_selector = null, array $selectors = [], ?array $extra_data = null ) {

if ( ! isset( $post->ID, $post->post_type )
|| empty( $rule )
Expand All @@ -66,6 +67,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
'rule' => $rule,
'ruletype' => $ruletype,
'object' => esc_attr( $rule_obj ),
'extra_data' => $extra_data ? wp_json_encode( $extra_data ) : null,
'recordcheck' => 1,
Comment on lines 67 to 71

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

$extra_data is JSON-encoded and stored with only sanitize_text_field() applied later, which does not validate structure or constrain values. Since this data originates from a client-provided REST payload, please whitelist expected keys and strictly sanitize/cast values (e.g., validate colors against a safe regex, cast ratios/font sizes to float/int) before encoding and persisting.

Copilot uses AI. Check for mistakes.
'user' => get_current_user_id(),
'ignre' => 0,
Expand Down Expand Up @@ -111,7 +113,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
$wpdb->query(
$wpdb->prepare(
'UPDATE %i SET recordcheck = %d, landmark = %s, landmark_selector = %s, object = %s, ancestry = %s, xpath = %s, ignre = %d WHERE siteid = %d and postid = %d and rule = %s and selector = %s and type = %s',
'UPDATE %i SET recordcheck = %d, landmark = %s, landmark_selector = %s, object = %s, ancestry = %s, xpath = %s, ignre = %d, extra_data = %s WHERE siteid = %d and postid = %d and rule = %s and selector = %s and type = %s',
$table_name,
1,
$rule_data['landmark'],
Expand All @@ -120,6 +122,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
$rule_data['ancestry'],
$rule_data['xpath'],
$rule_data['ignre'],
$rule_data['extra_data'],
$rule_data['siteid'],
$rule_data['postid'],
$rule_data['rule'],
Expand Down Expand Up @@ -160,6 +163,7 @@ public function insert( object $post, string $rule, string $ruletype, string $ru
'rule' => sanitize_text_field( $rule_data['rule'] ),
'ruletype' => sanitize_text_field( $rule_data['ruletype'] ),
'object' => esc_attr( $rule_data['object'] ),
'extra_data' => isset( $rule_data['extra_data'] ) ? sanitize_text_field( $rule_data['extra_data'] ) : 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.

medium

Using sanitize_text_field on a JSON string is not a robust method for sanitization and can lead to data corruption. If the JSON string contains characters like \u003c or \u003e, they will be stripped or encoded, potentially breaking the JSON structure upon parsing. While this may not affect the current data (color codes and numbers), it's a fragile approach.

A more secure and reliable method would be to decode the JSON string, sanitize each value within the resulting array using appropriate functions (e.g., sanitize_hex_color for colors, floatval for numbers), and then re-encode the array to a JSON string. This would prevent data corruption and add a layer of security against potential injection vulnerabilities, especially since this data is used to generate inline styles on the frontend.

'recordcheck' => absint( $rule_data['recordcheck'] ),
'user' => absint( $rule_data['user'] ),
'ignre' => absint( $rule_data['ignre'] ),
Expand Down
1 change: 1 addition & 0 deletions admin/class-update-database.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public function edac_update_database() {
rule text NOT NULL,
ruletype text NOT NULL,
object mediumtext NOT NULL,
extra_data text NULL,
recordcheck mediumint(9) NOT NULL,
created timestamp NOT NULL default CURRENT_TIMESTAMP,
user bigint(20) NOT NULL,
Expand Down
8 changes: 7 additions & 1 deletion includes/classes/class-rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,10 @@ public function set_post_scan_results( $request ) {
'ancestry' => $violation['ancestry'] ?? [],
'xpath' => $violation['xpath'] ?? [],
];
( new Insert_Rule_Data() )->insert( $post, $actual_rule_id, $impact, $html, $landmark, $landmark_selector, $selectors );

$extra_data = isset( $violation['extraData'] ) && is_array( $violation['extraData'] ) ? $violation['extraData'] : null;

( new Insert_Rule_Data() )->insert( $post, $actual_rule_id, $impact, $html, $landmark, $landmark_selector, $selectors, $extra_data );
Comment on lines +539 to +542

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

When accepting extraData from the REST request, it's only checked for is_array() and then passed through to DB storage. To prevent persisting untrusted/oversized payloads (and to support safe frontend rendering), validate that extraData contains only the expected scalar fields for contrast issues and sanitize/cast each value before calling insert().

Copilot uses AI. Check for mistakes.
Comment on lines +540 to +542

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.

⚠️ Potential issue | 🔴 Critical

Validate and normalize extraData before persisting it.

This field comes straight from the REST body and is later surfaced in the admin/front-end UIs (admin/class-ajax.php and src/frontendHighlighterApp/index.js). Storing arbitrary keys and strings here turns the new contrast metadata into a stored XSS/CSS-injection vector for anyone who can hit edit_post on the scanned post. Please whitelist the expected keys and cast each value to a safe format before calling Insert_Rule_Data::insert(). As per coding guidelines: Follow WordPress security best practices (sanitization, validation, nonces) in all PHP code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/classes/class-rest-api.php` around lines 540 - 542, The $extra_data
coming from the REST body must be validated and normalized before persisting: in
class-rest-api.php (before calling ( new Insert_Rule_Data() )->insert(...))
whitelist allowed keys (e.g. only accept the known keys used by
admin/class-ajax.php and src/frontendHighlighterApp/index.js), remove any
unknown keys, and coerce/escape each allowed value to a safe scalar (cast to
string/int/bool as expected, strip_tags/esc_html or use wp_kses_allowed_html for
limited HTML, and enforce length limits); if validation fails set $extra_data to
null or an empty array; then pass the sanitized $extra_data into
Insert_Rule_Data::insert(). Ensure you apply WordPress sanitization functions
and document the accepted keys in a comment.


/**
* Fires after a rule is run against the content.
Expand Down Expand Up @@ -992,6 +995,9 @@ private function process_rules_for_details( $rules, $post_id, $table_name, $site
}
$result['ignre_user_name'] = $user_cache[ $user_id ];
}
if ( isset( $result['extra_data'] ) && null !== $result['extra_data'] ) {
$result['extra_data'] = json_decode( $result['extra_data'], true );
Comment on lines +998 to +999

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

json_decode() is applied to extra_data without checking decode success. If the DB contains an empty string or malformed JSON, this will silently become null, which can make consumers handle extra_data inconsistently. Consider only replacing the value when decoding succeeds (check json_last_error()), and normalize the field to either null or an array consistently.

Suggested change
if ( isset( $result['extra_data'] ) && null !== $result['extra_data'] ) {
$result['extra_data'] = json_decode( $result['extra_data'], true );
if ( isset( $result['extra_data'] ) && '' !== $result['extra_data'] && null !== $result['extra_data'] ) {
$decoded_extra_data = json_decode( $result['extra_data'], true );
if ( JSON_ERROR_NONE === json_last_error() && is_array( $decoded_extra_data ) ) {
$result['extra_data'] = $decoded_extra_data;
} else {
$result['extra_data'] = null;
}
} else {
$result['extra_data'] = null;

Copilot uses AI. Check for mistakes.
}
$results_by_rule[ $rule_slug ][] = $result;
}

Expand Down
22 changes: 22 additions & 0 deletions src/frontendHighlighterApp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,28 @@ class AccessibilityCheckerHighlight {
</div>`;
}

// Color contrast data
if ( matchingObj.extra_data?.fgColor && matchingObj.extra_data?.bgColor ) {
const { fgColor, bgColor, contrastRatio, expectedContrastRatio } = matchingObj.extra_data;
content += `
<div class="edac-highlight-panel-description-contrast">
<div class="edac-highlight-panel-description-contrast-swatches">
<div class="edac-highlight-panel-description-contrast-swatch">
<div class="edac-highlight-panel-description-contrast-swatch-color" style="background-color: ${ fgColor }; color: ${ bgColor };">Aa</div>
<div class="edac-highlight-panel-description-contrast-swatch-label">${ __( 'Foreground', 'accessibility-checker' ) }<br>${ fgColor }</div>
</div>
<div class="edac-highlight-panel-description-contrast-swatch">
<div class="edac-highlight-panel-description-contrast-swatch-color" style="background-color: ${ bgColor }; color: ${ fgColor };">Aa</div>
<div class="edac-highlight-panel-description-contrast-swatch-label">${ __( 'Background', 'accessibility-checker' ) }<br>${ bgColor }</div>
</div>
</div>
<div class="edac-highlight-panel-description-contrast-ratio">
${ __( 'Contrast ratio:', 'accessibility-checker' ) } <strong>${ contrastRatio }:1</strong> (${ __( 'required:', 'accessibility-checker' ) } ${ expectedContrastRatio })
</div>
</div>
`;
Comment on lines +780 to +799

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

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

fgColor/bgColor/ratios are interpolated into an HTML string (including a style attribute) and then assigned via innerHTML. Because extra_data ultimately comes from scan results sent over the network and stored in the DB, this is an XSS risk if those fields are ever tampered with. Prefer building these nodes with createElement + textContent (and set styles only after validating allowed color formats), or at minimum escape the interpolated values before concatenating HTML.

Copilot uses AI. Check for mistakes.
}

if ( this.fixes[ matchingObj.slug ] && window.edacFrontendHighlighterApp?.userCanFix ) {
// this is the markup to put in the modal.
content += `
Expand Down
42 changes: 42 additions & 0 deletions src/frontendHighlighterApp/sass/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,48 @@ body {
margin-bottom: 5px !important;
}
}

&-contrast {
margin-top: 12px !important;
margin-bottom: 8px !important;

&-swatches {
display: flex !important;
gap: 8px !important;
margin-bottom: 8px !important;
}

&-swatch {
flex: 1 !important;

&-color {
height: 52px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 22px !important;
font-weight: bold !important;
border-radius: 4px !important;
border: 1px solid rgba(0, 0, 0, 0.25) !important;
}

&-label {
font-size: 11px !important;
margin-top: 4px !important;
text-align: center !important;
line-height: 1.4 !important;
}
}

&-ratio {
font-size: 13px !important;
margin-top: 4px !important;

strong {
font-weight: bold !important;
}
}
}
}

&-controls {
Expand Down
39 changes: 39 additions & 0 deletions src/issueModal/components/IssueDetailsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,45 @@ export const IssueDetailsModal = ( { issue, rule, onClose, isOpen, focusSection,
)
) }

{ /* Color Contrast Data */ }
{ issue?.extra_data?.fgColor && issue?.extra_data?.bgColor && (
<div className="edac-analysis__contrast" data-section="contrast">
<div className="edac-analysis__contrast-swatches">
<div className="edac-analysis__contrast-swatch">
<div
className="edac-analysis__contrast-swatch-color"
style={ { backgroundColor: issue.extra_data.fgColor, color: issue.extra_data.bgColor } }
aria-hidden="true"
>
Aa
</div>
<div className="edac-analysis__contrast-swatch-label">
<span>{ __( 'Foreground', 'accessibility-checker' ) }</span>
<code>{ issue.extra_data.fgColor }</code>
</div>
</div>
<div className="edac-analysis__contrast-swatch">
<div
className="edac-analysis__contrast-swatch-color"
style={ { backgroundColor: issue.extra_data.bgColor, color: issue.extra_data.fgColor } }
aria-hidden="true"
>
Aa
</div>
<div className="edac-analysis__contrast-swatch-label">
<span>{ __( 'Background', 'accessibility-checker' ) }</span>
<code>{ issue.extra_data.bgColor }</code>
</div>
</div>
</div>
<p className="edac-analysis__contrast-ratio">
{ __( 'Contrast ratio:', 'accessibility-checker' ) }{ ' ' }
<strong>{ issue.extra_data.contrastRatio }:1</strong>{ ' ' }
({ __( 'required:', 'accessibility-checker' ) } { issue.extra_data.expectedContrastRatio })
</p>
</div>
) }

<hr aria-hidden="true" />

{ /* Affected Code */ }
Expand Down
19 changes: 18 additions & 1 deletion src/pageScanner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,8 @@ function processViolation( violation, item ) {
const ancestry = violation.node.ancestry || [];
const xpath = violation.node.xpath || [];
const html = document.querySelector( selector )?.outerHTML;
return {

const result = {
selector,
ancestry,
xpath,
Expand All @@ -392,4 +393,20 @@ function processViolation( violation, item ) {
landmark: landmark.type,
landmarkSelector: landmark.selector,
};

if ( item.id === 'color_contrast_failure' ) {
const check = violation.any?.find( ( c ) => c.id === 'color-contrast' );
if ( check?.data ) {
result.extraData = {
fgColor: check.data.fgColor,
bgColor: check.data.bgColor,
contrastRatio: check.data.contrastRatio,
expectedContrastRatio: check.data.expectedContrastRatio,
fontSize: check.data.fontSize,
fontWeight: check.data.fontWeight,
};
}
}

return result;
}
Loading