security: validate billing address structure in Order model
Add billing address validation on Order creating/updating events. Required fields (line1, city, postcode, country) are enforced when commerce.checkout.require_billing_address is enabled (default). Unrecognised keys are stripped to prevent data pollution. Fixes #12 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b51084b8db
commit
aedc9ee378
1 changed files with 85 additions and 0 deletions
|
|
@ -16,6 +16,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
|
|
@ -51,11 +53,94 @@ class Order extends Model
|
|||
use HasFactory;
|
||||
use LogsActivity;
|
||||
|
||||
/**
|
||||
* Required keys for a valid billing address.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const BILLING_ADDRESS_REQUIRED_FIELDS = ['line1', 'city', 'postcode', 'country'];
|
||||
|
||||
/**
|
||||
* All recognised billing address keys.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public const BILLING_ADDRESS_ALLOWED_FIELDS = ['line1', 'line2', 'city', 'state', 'postcode', 'country'];
|
||||
|
||||
protected static function newFactory(): OrderFactory
|
||||
{
|
||||
return OrderFactory::new();
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
$validate = function (self $order): void {
|
||||
$order->validateBillingAddress();
|
||||
};
|
||||
|
||||
static::creating($validate);
|
||||
static::updating($validate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the billing_address structure.
|
||||
*
|
||||
* When `commerce.checkout.require_billing_address` is enabled (default),
|
||||
* the billing address must be present and contain the required fields.
|
||||
* When disabled, null is permitted but any non-null value must still
|
||||
* conform to the expected structure.
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validateBillingAddress(): void
|
||||
{
|
||||
$requireAddress = config('commerce.checkout.require_billing_address', true);
|
||||
$address = $this->billing_address;
|
||||
|
||||
// Null is acceptable only when billing address is not required
|
||||
if ($address === null) {
|
||||
if ($requireAddress) {
|
||||
throw ValidationException::withMessages([
|
||||
'billing_address' => ['Billing address is required.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-null value must be an array
|
||||
if (! is_array($address)) {
|
||||
throw ValidationException::withMessages([
|
||||
'billing_address' => ['Billing address must be an array.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$validator = Validator::make($address, [
|
||||
'line1' => ['required', 'string', 'max:255'],
|
||||
'line2' => ['nullable', 'string', 'max:255'],
|
||||
'city' => ['required', 'string', 'max:255'],
|
||||
'state' => ['nullable', 'string', 'max:255'],
|
||||
'postcode' => ['required', 'string', 'max:20'],
|
||||
'country' => ['required', 'string', 'size:2'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw ValidationException::withMessages(
|
||||
collect($validator->errors()->toArray())
|
||||
->mapWithKeys(fn (array $messages, string $key) => [
|
||||
"billing_address.{$key}" => $messages,
|
||||
])
|
||||
->all()
|
||||
);
|
||||
}
|
||||
|
||||
// Strip any unrecognised keys to prevent data pollution
|
||||
$this->billing_address = array_intersect_key(
|
||||
$address,
|
||||
array_flip(self::BILLING_ADDRESS_ALLOWED_FIELDS)
|
||||
);
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'orderable_type',
|
||||
'orderable_id',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue