Skip to content

Commit 93d5d3e

Browse files
mfilipcblanc
andauthored
fix(security): add input sanitization plugins for customer and quote … (#555)
Co-authored-by: Christopher Blanchard <chris@iddqd.org>
1 parent 4c2d977 commit 93d5d3e

File tree

5 files changed

+392
-1
lines changed

5 files changed

+392
-1
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
/**
3+
* Copyright (c) IDDQD Ltd
4+
*
5+
* Customer Address Sanitization Plugin
6+
*
7+
* This plugin intercepts customer address data and sanitizes the city field
8+
* to prevent XSS attacks by stripping HTML/script tags.
9+
*
10+
* @package Idealpostcodes_Ukaddresssearch
11+
* @author Ideal Postcodes <support@ideal-postcodes.co.uk>
12+
* @copyright Ideal Postcodes Ltd
13+
* @license MIT https://opensource.org/licenses/MIT
14+
* @link https://ideal-postcodes.co.uk
15+
*/
16+
17+
namespace Idealpostcodes\Ukaddresssearch\Plugin\Customer;
18+
19+
use Magento\Customer\Model\Address;
20+
21+
/**
22+
* Plugin to sanitize customer address data
23+
*
24+
* Intercepts the setCity method to strip HTML tags and prevent XSS attacks.
25+
*/
26+
class SanitizeAddressPlugin
27+
{
28+
/**
29+
* Sanitize city field before setting it on customer address
30+
*
31+
* @param Address $subject The customer address object
32+
* @param string|null $city The city value being set
33+
* @return array Modified arguments for the original method
34+
*/
35+
public function beforeSetCity(Address $subject, $city)
36+
{
37+
if ($city !== null && is_string($city)) {
38+
$city = $this->sanitizeText($city);
39+
}
40+
41+
return [$city];
42+
}
43+
44+
/**
45+
* Sanitize all address data before saving
46+
*
47+
* @param Address $subject The customer address object
48+
* @return void
49+
*/
50+
public function beforeBeforeSave(Address $subject)
51+
{
52+
// Sanitize city if it exists
53+
$city = $subject->getCity();
54+
if ($city !== null && is_string($city)) {
55+
$subject->setCity($this->sanitizeText($city));
56+
}
57+
58+
// Also sanitize other text fields
59+
$street = $subject->getStreet();
60+
if (is_array($street)) {
61+
$sanitizedStreet = array_map(function($line) {
62+
return is_string($line) ? $this->sanitizeText($line) : $line;
63+
}, $street);
64+
$subject->setStreet($sanitizedStreet);
65+
}
66+
67+
// Sanitize company name
68+
$company = $subject->getCompany();
69+
if ($company !== null && is_string($company)) {
70+
$subject->setCompany($this->sanitizeText($company));
71+
}
72+
}
73+
74+
/**
75+
* Sanitize text by removing HTML tags and JavaScript
76+
*
77+
* @param string $text
78+
* @return string
79+
*/
80+
private function sanitizeText($text)
81+
{
82+
if (!is_string($text)) {
83+
return $text;
84+
}
85+
86+
// Remove script/style tags AND their content
87+
$text = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $text);
88+
$text = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $text);
89+
90+
// Strip all remaining HTML and PHP tags
91+
$text = strip_tags($text);
92+
93+
// Remove any remaining JavaScript-like patterns
94+
$text = preg_replace('/javascript:/i', '', $text);
95+
$text = preg_replace('/on\w+\s*=/i', '', $text); // Remove event handlers
96+
97+
// Trim whitespace
98+
return trim($text);
99+
}
100+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
/**
3+
* Copyright (c) IDDQD Ltd
4+
*
5+
* Quote Address Sanitization Plugin
6+
*
7+
* This plugin intercepts quote address data (used in checkout) and sanitizes
8+
* the city field to prevent XSS attacks by stripping HTML/script tags.
9+
*
10+
* @package Idealpostcodes_Ukaddresssearch
11+
* @author Ideal Postcodes <support@ideal-postcodes.co.uk>
12+
* @copyright Ideal Postcodes Ltd
13+
* @license MIT https://opensource.org/licenses/MIT
14+
* @link https://ideal-postcodes.co.uk
15+
*/
16+
17+
namespace Idealpostcodes\Ukaddresssearch\Plugin\Quote;
18+
19+
use Magento\Quote\Model\Quote\Address;
20+
21+
/**
22+
* Plugin to sanitize address data in checkout
23+
*
24+
* Intercepts the setCity method to strip HTML tags and prevent XSS attacks.
25+
* This is necessary because checkout uses REST API/GraphQL which bypasses
26+
* the standard EAV input_filter attribute setting.
27+
*/
28+
class SanitizeAddressPlugin
29+
{
30+
/**
31+
* Sanitize city field before setting it on quote address
32+
*
33+
* This plugin intercepts the setCity() method call and strips any HTML/XML
34+
* tags from the input to prevent stored XSS attacks.
35+
*
36+
* @param Address $subject The quote address object
37+
* @param string|null $city The city value being set
38+
* @return array Modified arguments for the original method
39+
*/
40+
public function beforeSetCity(Address $subject, $city)
41+
{
42+
if ($city !== null && is_string($city)) {
43+
// Remove script/style tags AND their content
44+
$city = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $city);
45+
$city = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $city);
46+
47+
// Strip all remaining HTML and PHP tags
48+
$city = strip_tags($city);
49+
50+
// Remove any remaining JavaScript-like patterns
51+
$city = preg_replace('/javascript:/i', '', $city);
52+
$city = preg_replace('/on\w+\s*=/i', '', $city); // Remove event handlers like onclick=
53+
54+
// Trim whitespace
55+
$city = trim($city);
56+
}
57+
58+
return [$city];
59+
}
60+
61+
/**
62+
* Sanitize all address data before saving
63+
*
64+
* This provides an additional layer of protection by sanitizing
65+
* all address fields that might contain user input.
66+
*
67+
* @param Address $subject The quote address object
68+
* @return void
69+
*/
70+
public function beforeBeforeSave(Address $subject)
71+
{
72+
// Sanitize city if it exists
73+
$city = $subject->getCity();
74+
if ($city !== null && is_string($city)) {
75+
$subject->setCity($this->sanitizeText($city));
76+
}
77+
78+
// Also sanitize other text fields that might be vulnerable
79+
$street = $subject->getStreet();
80+
if (is_array($street)) {
81+
$sanitizedStreet = array_map(function($line) {
82+
return is_string($line) ? $this->sanitizeText($line) : $line;
83+
}, $street);
84+
$subject->setStreet($sanitizedStreet);
85+
}
86+
87+
// Sanitize company name
88+
$company = $subject->getCompany();
89+
if ($company !== null && is_string($company)) {
90+
$subject->setCompany($this->sanitizeText($company));
91+
}
92+
}
93+
94+
/**
95+
* Sanitize text by removing HTML tags and JavaScript
96+
*
97+
* @param string $text
98+
* @return string
99+
*/
100+
private function sanitizeText($text)
101+
{
102+
if (!is_string($text)) {
103+
return $text;
104+
}
105+
106+
// Remove script/style tags AND their content
107+
$text = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $text);
108+
$text = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $text);
109+
110+
// Strip all remaining HTML and PHP tags
111+
$text = strip_tags($text);
112+
113+
// Remove any remaining JavaScript-like patterns
114+
$text = preg_replace('/javascript:/i', '', $text);
115+
$text = preg_replace('/on\w+\s*=/i', '', $text); // Remove event handlers
116+
117+
// Trim whitespace
118+
return trim($text);
119+
}
120+
}

Setup/UpgradeData.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
/**
3+
* Copyright (c) IDDQD Ltd
4+
*
5+
* Ideal Postcodes Magento Extension
6+
*
7+
* This extension provides UK address search functionality for Magento 2
8+
* checkout and customer address forms using the Ideal Postcodes API.
9+
*
10+
* @package Idealpostcodes_Ukaddresssearch
11+
* @author Ideal Postcodes <support@ideal-postcodes.co.uk>
12+
* @copyright IDDQD Ltd
13+
* @license MIT https://opensource.org/licenses/MIT
14+
* @link https://ideal-postcodes.co.uk
15+
*/
16+
17+
namespace Idealpostcodes\Ukaddresssearch\Setup;
18+
19+
use Magento\Framework\Setup\UpgradeDataInterface;
20+
use Magento\Framework\Setup\ModuleContextInterface;
21+
use Magento\Framework\Setup\ModuleDataSetupInterface;
22+
use Magento\Customer\Setup\CustomerSetupFactory;
23+
use Magento\Customer\Api\AddressMetadataInterface;
24+
25+
/**
26+
* Upgrade Data Script
27+
*
28+
* This script updates the city attribute validation rules to allow
29+
* real-world city names with periods (e.g., "St. Helen"), numbers,
30+
* ampersands, and other common punctuation marks.
31+
*
32+
* The default Magento validation regex is too restrictive and only
33+
* allows letters and spaces, which fails for cities like:
34+
* - St. Moritz
35+
* - Brighton & Hove
36+
* - 29 Palms
37+
*
38+
* This override implements a more permissive pattern while maintaining
39+
* security and data integrity.
40+
*/
41+
class UpgradeData implements UpgradeDataInterface
42+
{
43+
/**
44+
* @var CustomerSetupFactory
45+
*/
46+
protected $customerSetupFactory;
47+
48+
/**
49+
* Constructor
50+
*
51+
* @param CustomerSetupFactory $customerSetupFactory
52+
*/
53+
public function __construct(CustomerSetupFactory $customerSetupFactory)
54+
{
55+
$this->customerSetupFactory = $customerSetupFactory;
56+
}
57+
58+
/**
59+
* Upgrade data for the module
60+
*
61+
* @param ModuleDataSetupInterface $setup
62+
* @param ModuleContextInterface $context
63+
* @return void
64+
*/
65+
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
66+
{
67+
// Apply city validation override for version 3.1.3
68+
if (version_compare($context->getVersion(), '3.1.3', '<')) {
69+
$this->updateCityValidationRules($setup);
70+
}
71+
}
72+
73+
/**
74+
* Update city attribute validation rules
75+
*
76+
* Overrides the default city validation regex to allow:
77+
* - Letters (A-Z, a-z)
78+
* - Numbers (0-9)
79+
* - Spaces
80+
* - Hyphens (-)
81+
* - Periods/dots (.)
82+
* - Ampersands (&)
83+
* - Parentheses ()
84+
* - Apostrophes (')
85+
*
86+
* This allows for real-world city names like:
87+
* - "St. Helen", "St. Moritz" (periods in abbreviations)
88+
* - "Brighton & Hove" (ampersands)
89+
* - "29 Palms" (numbers)
90+
* - "King's Lynn" (apostrophes)
91+
* - "Ashton-under-Lyne" (hyphens)
92+
*
93+
* @param ModuleDataSetupInterface $setup
94+
* @return void
95+
*/
96+
protected function updateCityValidationRules(ModuleDataSetupInterface $setup)
97+
{
98+
$setup->startSetup();
99+
100+
$customerSetup = $this->customerSetupFactory->create(['setup' => $setup]);
101+
102+
// Get the address entity type ID
103+
$addressEntityTypeId = $customerSetup->getEntityTypeId(
104+
AddressMetadataInterface::ENTITY_TYPE_ADDRESS
105+
);
106+
107+
// Get the current city attribute configuration
108+
$cityAttribute = $customerSetup->getAttribute($addressEntityTypeId, 'city');
109+
110+
if ($cityAttribute) {
111+
// Get existing validation rules
112+
$validateRules = $cityAttribute['validate_rules'] ?? [];
113+
114+
// Decode JSON if it's stored as a string
115+
if (is_string($validateRules)) {
116+
$validateRules = json_decode($validateRules, true) ?: [];
117+
}
118+
119+
// Remove restrictive validation rules
120+
// Keep only max_text_length and min_text_length
121+
$validateRules = [
122+
'max_text_length' => 255,
123+
'min_text_length' => 1,
124+
'input_validation' => 'length' // Only validate length, not character type
125+
];
126+
127+
// Update the attribute with relaxed validation rules
128+
// This removes the validate-alpha restriction
129+
// input_filter strips HTML tags to prevent XSS attacks
130+
$customerSetup->updateAttribute(
131+
$addressEntityTypeId,
132+
'city',
133+
[
134+
'validate_rules' => json_encode($validateRules, JSON_UNESCAPED_SLASHES),
135+
'frontend_class' => null, // Remove any frontend validation classes
136+
'input_filter' => 'striptags' // Strip HTML/XML tags for XSS protection
137+
]
138+
);
139+
}
140+
141+
$setup->endSetup();
142+
}
143+
}

etc/di.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Copyright (c) IDDQD Ltd
5+
*
6+
* Dependency Injection Configuration
7+
*
8+
* This file configures plugins to sanitize address data and prevent XSS attacks
9+
* in checkout and address management forms.
10+
*/
11+
-->
12+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
13+
<!-- Plugin to sanitize quote address data (checkout) -->
14+
<type name="Magento\Quote\Model\Quote\Address">
15+
<plugin name="idealpostcodes_sanitize_quote_address"
16+
type="Idealpostcodes\Ukaddresssearch\Plugin\Quote\SanitizeAddressPlugin"
17+
sortOrder="10"/>
18+
</type>
19+
20+
<!-- Plugin to sanitize customer address data -->
21+
<type name="Magento\Customer\Model\Address">
22+
<plugin name="idealpostcodes_sanitize_customer_address"
23+
type="Idealpostcodes\Ukaddresssearch\Plugin\Customer\SanitizeAddressPlugin"
24+
sortOrder="10"/>
25+
</type>
26+
</config>

0 commit comments

Comments
 (0)