Skip to content

Commit a27a1d7

Browse files
authored
New. Settings. Add CSP nonce support. (#868)
* New. Settings. Add CSP nonce support. * fix cp * fix psalm * fix cp * fix cp
1 parent d9bcedf commit a27a1d7

8 files changed

Lines changed: 181 additions & 49 deletions

File tree

cleantalk.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,11 +615,11 @@ function apbct_write_js_errors($data)
615615

616616

617617
add_action('mec_booking_end_form_step_2', function () {
618-
echo "<script>
619-
if (typeof ctPublic.force_alt_cookies == 'undefined' || (ctPublic.force_alt_cookies !== 'undefined' && !ctPublic.force_alt_cookies)) {
618+
echo apbct_get_inline_script_tag(
619+
"if (typeof ctPublic.force_alt_cookies == 'undefined' || (ctPublic.force_alt_cookies !== 'undefined' && !ctPublic.force_alt_cookies)) {
620620
ctNoCookieAttachHiddenFieldsToForms();
621-
}
622-
</script>";
621+
}"
622+
);
623623
});
624624

625625
// Public actions

inc/cleantalk-pluggable.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2298,3 +2298,67 @@ function apbct_get_email_encoder_pass_key()
22982298

22992299
return md5(Helper::ipGet() . $apbct->api_key . 'email_encoder');
23002300
}
2301+
2302+
/**
2303+
* Returns CSP nonce for CleanTalk inline scripts.
2304+
*
2305+
* @return string
2306+
*/
2307+
function apbct_get_csp_nonce()
2308+
{
2309+
/**
2310+
* Filter CSP nonce for CleanTalk inline scripts.
2311+
*
2312+
* @param string $nonce CSP nonce value for script tags.
2313+
*/
2314+
return (string) apply_filters('apbct_csp_nonce', '');
2315+
}
2316+
2317+
/**
2318+
* Returns inline script tag with optional CSP nonce.
2319+
*
2320+
* @param string $javascript JavaScript code.
2321+
* @param array<string, string|bool> $attributes Script tag attributes.
2322+
*
2323+
* @return string
2324+
*/
2325+
function apbct_get_inline_script_tag($javascript, $attributes = array())
2326+
{
2327+
$nonce = apbct_get_csp_nonce();
2328+
if ( $nonce !== '' ) {
2329+
$attributes['nonce'] = $nonce;
2330+
}
2331+
2332+
if ( function_exists('wp_get_inline_script_tag') ) {
2333+
return wp_get_inline_script_tag($javascript, $attributes);
2334+
}
2335+
2336+
$attr_string = '';
2337+
foreach ( $attributes as $name => $value ) {
2338+
if ( $value === true ) {
2339+
$attr_string .= ' ' . esc_attr($name);
2340+
} elseif ( $value !== false && $value !== null && $value !== '' ) {
2341+
$attr_string .= ' ' . esc_attr($name) . '="' . esc_attr((string) $value) . '"';
2342+
}
2343+
}
2344+
2345+
$javascript = preg_replace('#</script#i', '<\/script', $javascript);
2346+
2347+
return '<script' . $attr_string . '>' . $javascript . '</script>';
2348+
}
2349+
2350+
/**
2351+
* Returns allowed HTML tags for inline CleanTalk scripts passed through kses.
2352+
*
2353+
* @return array<string, array<string, bool>>
2354+
*/
2355+
function apbct_get_inline_script_kses()
2356+
{
2357+
return array(
2358+
'script' => array(
2359+
'type' => true,
2360+
'data-cookieconsent' => true,
2361+
'nonce' => true,
2362+
),
2363+
);
2364+
}

inc/cleantalk-public.php

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -594,28 +594,25 @@ function apbct_hook__wp_footer()
594594
}";
595595
}
596596

597-
$cookie_bot_asset = (class_exists('Cookiebot_WP')) ? 'data-cookieconsent="ignore"' : '';
597+
$script_attrs = array();
598+
if ( class_exists('Cookiebot_WP') ) {
599+
$script_attrs['data-cookieconsent'] = 'ignore';
600+
}
598601

599-
$script =
600-
'<script ' . $cookie_bot_asset
601-
. ">
602-
document.addEventListener('DOMContentLoaded', function () {
602+
$script = apbct_get_inline_script_tag(
603+
"document.addEventListener('DOMContentLoaded', function () {
603604
setTimeout(function(){
604605
if( document.querySelectorAll('[name^=ct_checkjs]').length > 0 ) {
605606
" . $send_way_asset . "
606607
}
607-
}," . $timeout . ")
608-
})
609-
</script>";
608+
}," . $timeout . ")
609+
})",
610+
$script_attrs
611+
);
610612

611613
echo Escape::escKses(
612614
$script,
613-
array(
614-
'script' => array(
615-
'type' => true,
616-
'data-cookieconsent' => true
617-
)
618-
)
615+
apbct_get_inline_script_kses()
619616
);
620617
}
621618
}
@@ -674,18 +671,24 @@ function ct_add_hidden_fields(
674671
return;
675672
}
676673

674+
$script_attrs = array();
675+
if ( class_exists('Cookiebot_WP') ) {
676+
$script_attrs['data-cookieconsent'] = 'ignore';
677+
}
678+
677679
$ct_input_challenge = sprintf("'%s'", is_null($ct_checkjs_key) ? $ct_checkjs_def : $ct_checkjs_key);
678680
$field_id = $field_name . '_' . $field_id_hash;
679-
$html = "<input type=\"hidden\" id=\"{$field_id}\" name=\"{$field_name}\" value=\"{$ct_checkjs_def}\" />
680-
<script " . (class_exists('Cookiebot_WP') ? 'data-cookieconsent="ignore"' : '') . ">
681-
setTimeout(function(){
681+
$html = "<input type=\"hidden\" id=\"{$field_id}\" name=\"{$field_name}\" value=\"{$ct_checkjs_def}\" />"
682+
. apbct_get_inline_script_tag(
683+
"setTimeout(function(){
682684
var ct_input_name = \"{$field_id}\";
683685
if (document.getElementById(ct_input_name) !== null) {
684686
var ct_input_value = document.getElementById(ct_input_name).value;
685687
document.getElementById(ct_input_name).value = document.getElementById(ct_input_name).value.replace(ct_input_value, {$ct_input_challenge});
686688
}
687-
}, 1000);
688-
</script>";
689+
}, 1000);",
690+
$script_attrs
691+
);
689692
}
690693

691694
// Simplify JS code and Fixing issue with wpautop()
@@ -696,17 +699,16 @@ function ct_add_hidden_fields(
696699
} else {
697700
echo Escape::escKses(
698701
$html,
699-
array(
700-
'script' => array(
701-
'type' => true,
702-
'data-cookieconsent' => true
703-
),
702+
array_merge(
703+
apbct_get_inline_script_kses(),
704+
array(
704705
'input' => array(
705706
'type' => true,
706707
'id' => true,
707708
'name' => true,
708709
'value' => true
709710
)
711+
)
710712
)
711713
);
712714
}

lib/Cleantalk/Antispam/Integrations/CleantalkExternalForms.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ private function constructOriginExternalForm($action, $method)
163163
</html>';
164164

165165
// Cookiebot chunk
166-
$bot_chunk = class_exists('Cookiebot_WP') ? 'data-cookieconsent="ignore"' : '';
166+
$bot_attrs = class_exists('Cookiebot_WP') ? array('data-cookieconsent' => 'ignore') : array();
167167

168168
// HTML form clearing script
169-
$script = "<script " . $bot_chunk . ">
170-
let form = document.forms[0];
169+
$script = apbct_get_inline_script_tag(
170+
"let form = document.forms[0];
171171
let availabilitySubmit = false;
172172
for (let i = 0; i < form.length; i++) {
173173
let typeElem = form[i].getAttribute('type');
@@ -182,9 +182,9 @@ private function constructOriginExternalForm($action, $method)
182182
form.removeChild(objects[0]);
183183
}
184184
}
185-
form.submit();
186-
</script>
187-
";
185+
form.submit();",
186+
$bot_attrs
187+
);
188188

189189
$form_template = str_replace('%METHOD', $method, $form_template);
190190
$form_template = str_replace('%ACTION', $action, $form_template);

lib/Cleantalk/ApbctWP/Firewall/AntiCrawler.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,12 +459,11 @@ public static function setCookie()
459459
{
460460
global $apbct;
461461

462-
$script =
463-
"<script>
464-
window.addEventListener('DOMContentLoaded', function () {
462+
$script = apbct_get_inline_script_tag(
463+
"window.addEventListener('DOMContentLoaded', function () {
465464
ctSetCookie( " . json_encode(self::COOKIE_NAME__ANTIBOT) . ", '" . apbct_get_anti_bot_cookie_hash($apbct->api_key, $apbct->data['salt']) . "', 0 );
466-
});
467-
</script>";
465+
});"
466+
);
468467

469468
echo $script;
470469
}

lib/Cleantalk/ApbctWP/Localize/CtPublicFunctionsLocalize.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,15 @@ public static function getData()
3737

3838
public static function getCode()
3939
{
40-
return '
41-
<script data-no-defer="1" data-ezscrex="false" data-cfasync="false" data-pagespeed-no-defer data-cookieconsent="ignore">
42-
var ' . self::NAME . ' = ' . json_encode(self::getData()) . '
43-
</script>
44-
';
40+
return apbct_get_inline_script_tag(
41+
'var ' . self::NAME . ' = ' . json_encode(self::getData()),
42+
array(
43+
'data-no-defer' => '1',
44+
'data-ezscrex' => 'false',
45+
'data-cfasync' => 'false',
46+
'data-pagespeed-no-defer' => true,
47+
'data-cookieconsent' => 'ignore',
48+
)
49+
);
4550
}
4651
}

lib/Cleantalk/ApbctWP/Localize/CtPublicLocalize.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ public static function getData()
5050

5151
public static function getCode()
5252
{
53-
return '
54-
<script data-no-defer="1" data-ezscrex="false" data-cfasync="false" data-pagespeed-no-defer data-cookieconsent="ignore">
55-
var ' . self::NAME . ' = ' . json_encode(self::getData()) . '
56-
</script>
57-
';
53+
return apbct_get_inline_script_tag(
54+
'var ' . self::NAME . ' = ' . json_encode(self::getData()),
55+
array(
56+
'data-no-defer' => '1',
57+
'data-ezscrex' => 'false',
58+
'data-cfasync' => 'false',
59+
'data-pagespeed-no-defer' => true,
60+
'data-cookieconsent' => 'ignore',
61+
)
62+
);
5863
}
5964
}

tests/ApbctWP/TestCspNonce.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
use Cleantalk\ApbctWP\State;
4+
5+
class TestCspNonce extends \PHPUnit\Framework\TestCase
6+
{
7+
/** @var mixed */
8+
private $original_apbct;
9+
10+
protected function setUp(): void
11+
{
12+
parent::setUp();
13+
global $apbct;
14+
$this->original_apbct = $apbct;
15+
$apbct = new State('cleantalk', array('settings', 'data', 'errors', 'remote_calls', 'stats', 'fw_stats'));
16+
}
17+
18+
protected function tearDown(): void
19+
{
20+
remove_all_filters('apbct_csp_nonce');
21+
global $apbct;
22+
$apbct = $this->original_apbct;
23+
parent::tearDown();
24+
}
25+
26+
public function testInlineScriptTagWithoutNonce()
27+
{
28+
$tag = apbct_get_inline_script_tag('var test = 1;');
29+
30+
$this->assertStringContainsString('var test = 1;', $tag);
31+
$this->assertStringNotContainsString('nonce=', $tag);
32+
}
33+
34+
public function testInlineScriptTagWithNonceFilter()
35+
{
36+
add_filter('apbct_csp_nonce', function () {
37+
return 'test-nonce-value';
38+
});
39+
40+
$tag = apbct_get_inline_script_tag('var test = 1;');
41+
42+
$this->assertStringContainsString('nonce="test-nonce-value"', $tag);
43+
}
44+
45+
public function testLocalizeScriptsIncludeNonceWhenFilterIsSet()
46+
{
47+
add_filter('apbct_csp_nonce', function () {
48+
return 'localize-nonce';
49+
});
50+
51+
$functions_tag = \Cleantalk\ApbctWP\Localize\CtPublicFunctionsLocalize::getCode();
52+
$public_tag = \Cleantalk\ApbctWP\Localize\CtPublicLocalize::getCode();
53+
54+
$this->assertStringContainsString('nonce="localize-nonce"', $functions_tag);
55+
$this->assertStringContainsString('nonce="localize-nonce"', $public_tag);
56+
}
57+
}

0 commit comments

Comments
 (0)