Skip to content

Commit d34f33e

Browse files
authored
Merge pull request #741 from CleanTalk/wc_blk_ord_xss_av
Fix. Code. Escaping woocommerce order data
2 parents 535adb9 + cb6744b commit d34f33e

File tree

2 files changed

+190
-5
lines changed

2 files changed

+190
-5
lines changed

lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ private function renderOrderDetailsColumn($order_details)
219219
$wc_product_class = '\WC_Product';
220220
$product_title = $wc_product instanceof $wc_product_class ? $wc_product->get_title() : '';
221221
}
222-
$result .= "<b>" . $product_title . "</b>";
222+
$result .= "<b>" . esc_html($product_title) . "</b>";
223223
$result .= " - ";
224-
$result .= $order_detail['quantity'];
224+
$result .= esc_html($order_detail['quantity']);
225225
$result .= "<br>";
226226
}
227227

@@ -242,11 +242,11 @@ private function renderCustomerDetailsColumn($customer_details)
242242

243243
$result = '';
244244

245-
$result .= "<b>" . ($customer_details["billing_first_name"] ?? '') . "</b>";
245+
$result .= "<b>" . esc_html($customer_details["billing_first_name"] ?? '') . "</b>";
246246
$result .= "<br>";
247-
$result .= "<b>" . ($customer_details["billing_last_name"] ?? '') . "</b>";
247+
$result .= "<b>" . esc_html($customer_details["billing_last_name"] ?? '') . "</b>";
248248
$result .= "<br>";
249-
$result .= "<b>" . ($customer_details["billing_email"] ?? '') . "</b>";
249+
$result .= "<b>" . esc_html($customer_details["billing_email"] ?? '') . "</b>";
250250

251251
return $result;
252252
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
use Cleantalk\ApbctWP\WcSpamOrdersListTable;
4+
use PHPUnit\Framework\TestCase;
5+
6+
class TestWcSpamOrdersListTable extends TestCase
7+
{
8+
/**
9+
* @var WcSpamOrdersListTable
10+
*/
11+
private $instance;
12+
13+
/**
14+
* @var \ReflectionMethod
15+
*/
16+
private $renderOrderDetailsColumn;
17+
18+
/**
19+
* @var \ReflectionMethod
20+
*/
21+
private $renderCustomerDetailsColumn;
22+
23+
protected function setUp(): void
24+
{
25+
// Create instance without calling constructor to avoid dependencies
26+
$reflection = new ReflectionClass(WcSpamOrdersListTable::class);
27+
$this->instance = $reflection->newInstanceWithoutConstructor();
28+
29+
// Make private methods accessible
30+
$this->renderOrderDetailsColumn = $reflection->getMethod('renderOrderDetailsColumn');
31+
$this->renderOrderDetailsColumn->setAccessible(true);
32+
33+
$this->renderCustomerDetailsColumn = $reflection->getMethod('renderCustomerDetailsColumn');
34+
$this->renderCustomerDetailsColumn->setAccessible(true);
35+
}
36+
37+
/**
38+
* Test renderOrderDetailsColumn returns error on invalid JSON
39+
*/
40+
public function testRenderOrderDetailsColumnInvalidJson()
41+
{
42+
$result = $this->renderOrderDetailsColumn->invoke($this->instance, 'invalid json');
43+
44+
$this->assertEquals('<b>Product details decoding error.</b><br>', $result);
45+
}
46+
47+
/**
48+
* Test renderOrderDetailsColumn returns error on non-array JSON
49+
*/
50+
public function testRenderOrderDetailsColumnNonArrayJson()
51+
{
52+
$result = $this->renderOrderDetailsColumn->invoke($this->instance, '"just a string"');
53+
54+
$this->assertEquals('<b>Product details decoding error.</b><br>', $result);
55+
}
56+
57+
/**
58+
* Test renderOrderDetailsColumn with valid order details (no WooCommerce)
59+
*/
60+
public function testRenderOrderDetailsColumnValidDataWithoutWooCommerce()
61+
{
62+
$orderDetails = json_encode([
63+
['product_id' => 1, 'quantity' => 2],
64+
['product_id' => 2, 'quantity' => 5],
65+
]);
66+
67+
$result = $this->renderOrderDetailsColumn->invoke($this->instance, $orderDetails);
68+
69+
// Without WooCommerce, product title should be 'Unavailable product'
70+
$this->assertStringContainsString('<b>Unavailable product</b>', $result);
71+
$this->assertStringContainsString(' - 2<br>', $result);
72+
$this->assertStringContainsString(' - 5<br>', $result);
73+
}
74+
75+
/**
76+
* Test renderOrderDetailsColumn escapes HTML to prevent XSS
77+
*/
78+
public function testRenderOrderDetailsColumnEscapesHtmlInQuantity()
79+
{
80+
$orderDetails = json_encode([
81+
['product_id' => 1, 'quantity' => '<script>alert("test")</script>'],
82+
]);
83+
84+
$result = $this->renderOrderDetailsColumn->invoke($this->instance, $orderDetails);
85+
86+
// Verify that script tags are escaped
87+
$this->assertStringNotContainsString('<script>', $result);
88+
$this->assertStringContainsString('&lt;script&gt;', $result);
89+
}
90+
91+
/**
92+
* Test renderOrderDetailsColumn with empty array
93+
*/
94+
public function testRenderOrderDetailsColumnEmptyArray()
95+
{
96+
$orderDetails = json_encode([]);
97+
98+
$result = $this->renderOrderDetailsColumn->invoke($this->instance, $orderDetails);
99+
100+
$this->assertEquals('', $result);
101+
}
102+
103+
/**
104+
* Test renderCustomerDetailsColumn returns error on invalid JSON
105+
*/
106+
public function testRenderCustomerDetailsColumnInvalidJson()
107+
{
108+
$result = $this->renderCustomerDetailsColumn->invoke($this->instance, 'invalid json');
109+
110+
$this->assertEquals('<b>Customer details decoding error.</b><br>', $result);
111+
}
112+
113+
/**
114+
* Test renderCustomerDetailsColumn with valid customer details
115+
*/
116+
public function testRenderCustomerDetailsColumnValidData()
117+
{
118+
$customerDetails = json_encode([
119+
'billing_first_name' => 'John',
120+
'billing_last_name' => 'Doe',
121+
'billing_email' => 'john@example.com',
122+
]);
123+
124+
$result = $this->renderCustomerDetailsColumn->invoke($this->instance, $customerDetails);
125+
126+
$this->assertStringContainsString('<b>John</b>', $result);
127+
$this->assertStringContainsString('<b>Doe</b>', $result);
128+
$this->assertStringContainsString('<b>john@example.com</b>', $result);
129+
}
130+
131+
/**
132+
* Test renderCustomerDetailsColumn escapes HTML to prevent XSS
133+
*/
134+
public function testRenderCustomerDetailsColumnEscapesHtml()
135+
{
136+
$customerDetails = json_encode([
137+
'billing_first_name' => '<script>alert("test")</script>',
138+
'billing_last_name' => '<img onerror="alert(1)" src="">',
139+
'billing_email' => '"><script>alert("email")</script>',
140+
]);
141+
142+
$result = $this->renderCustomerDetailsColumn->invoke($this->instance, $customerDetails);
143+
144+
// Verify that malicious HTML is escaped
145+
$this->assertStringNotContainsString('<script>', $result);
146+
$this->assertStringNotContainsString('<img', $result);
147+
$this->assertStringContainsString('&lt;script&gt;', $result);
148+
$this->assertStringContainsString('&lt;img', $result);
149+
}
150+
151+
/**
152+
* Test renderCustomerDetailsColumn with missing fields
153+
*/
154+
public function testRenderCustomerDetailsColumnMissingFields()
155+
{
156+
$customerDetails = json_encode([
157+
'billing_first_name' => 'John',
158+
// billing_last_name and billing_email are missing
159+
]);
160+
161+
$result = $this->renderCustomerDetailsColumn->invoke($this->instance, $customerDetails);
162+
163+
$this->assertStringContainsString('<b>John</b>', $result);
164+
// Empty values for missing fields
165+
$this->assertStringContainsString('<b></b>', $result);
166+
}
167+
168+
/**
169+
* Test renderCustomerDetailsColumn with empty values
170+
*/
171+
public function testRenderCustomerDetailsColumnEmptyValues()
172+
{
173+
$customerDetails = json_encode([
174+
'billing_first_name' => '',
175+
'billing_last_name' => '',
176+
'billing_email' => '',
177+
]);
178+
179+
$result = $this->renderCustomerDetailsColumn->invoke($this->instance, $customerDetails);
180+
181+
// Should contain empty bold tags
182+
$expectedCount = substr_count($result, '<b></b>');
183+
$this->assertEquals(3, $expectedCount);
184+
}
185+
}

0 commit comments

Comments
 (0)