';
// JavaScript data
wp_localize_script('mlf-admin', 'mlfData', [
'fontName' => esc_js($font_name), // Or let wp_localize_script handle it
]);
// Translatable strings with variables
printf(
esc_html__('Installed: %s', 'maple-local-fonts'),
esc_html($font_name)
);
```
**Never trust input for output:**
```php
// WRONG - XSS vulnerability
echo '' . $_POST['font_name'] . '
';
// RIGHT - sanitize input, escape output
$font_name = sanitize_text_field($_POST['font_name']);
echo '' . esc_html($font_name) . '
';
```
### A8 - Insecure Deserialization
```php
// NEVER use unserialize() on external data
$data = unserialize($_POST['data']); // DANGEROUS
// Use JSON instead
$data = json_decode(sanitize_text_field($_POST['data']), true);
if (json_last_error() !== JSON_ERROR_NONE) {
wp_send_json_error(['message' => 'Invalid data format']);
}
```
### A9 - Vulnerable Components
- No external PHP libraries
- Use only WordPress core functions
- Keep dependencies to zero
---
## Nonce Implementation
### Creating Nonces
**In admin page form:**
```php
wp_nonce_field('mlf_download_font', 'mlf_nonce');
```
**For AJAX (via wp_localize_script):**
```php
wp_localize_script('mlf-admin', 'mlfData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('mlf_download_font'),
]);
```
### Verifying Nonces
**AJAX handler:**
```php
// Returns false on failure, doesn't die (we handle response ourselves)
if (!check_ajax_referer('mlf_download_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
```
**Form submission:**
```php
if (!wp_verify_nonce($_POST['mlf_nonce'], 'mlf_download_font')) {
wp_die('Security check failed');
}
```
### Nonce Names
Use consistent, descriptive nonce action names:
| Action | Nonce Name |
|--------|------------|
| Download font | `mlf_download_font` |
| Delete font | `mlf_delete_font` |
| Update settings | `mlf_update_settings` |
---
## Input Validation
### Font Name Validation
```php
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
// Strict allowlist pattern - alphanumeric, spaces, hyphens only
if (!preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name)) {
wp_send_json_error(['message' => 'Invalid font name: only letters, numbers, spaces, and hyphens allowed']);
}
// Length limit
if (strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Font name too long']);
}
// Not empty
if (empty($font_name)) {
wp_send_json_error(['message' => 'Font name required']);
}
```
### Weight Validation
```php
$weights = isset($_POST['weights']) ? (array) $_POST['weights'] : [];
// Convert to integers
$weights = array_map('absint', $weights);
// Strict allowlist
$allowed_weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];
$weights = array_intersect($weights, $allowed_weights);
// Must have at least one
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
```
### Style Validation
```php
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
// Strict allowlist - only these two values ever
$allowed_styles = ['normal', 'italic'];
$styles = array_filter($styles, function($style) use ($allowed_styles) {
return in_array($style, $allowed_styles, true);
});
// Must have at least one
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
```
### Font Family ID Validation (for delete)
```php
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify it exists and is a font family
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
// Verify it's one we imported (not a theme font)
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
```
---
## File Operation Security
### Path Traversal Prevention
```php
/**
* Validate that a path is within the WordPress fonts directory.
* Prevents path traversal attacks.
*
* @param string $path Full path to validate
* @return bool True if path is safe, false otherwise
*/
function mlf_validate_font_path($path) {
$font_dir = wp_get_font_dir();
$fonts_path = wp_normalize_path(trailingslashit($font_dir['path']));
// Resolve to real path (handles ../ etc)
$real_path = realpath($path);
// If realpath fails, file doesn't exist yet - validate the directory
if ($real_path === false) {
$dir = dirname($path);
$real_dir = realpath($dir);
if ($real_dir === false) {
return false;
}
$real_path = wp_normalize_path($real_dir . '/' . basename($path));
} else {
$real_path = wp_normalize_path($real_path);
}
// Must be within fonts directory
return strpos($real_path, $fonts_path) === 0;
}
```
### Filename Sanitization
```php
/**
* Sanitize and validate a font filename.
*
* @param string $filename The filename to validate
* @return string|false Sanitized filename or false if invalid
*/
function mlf_sanitize_font_filename($filename) {
// WordPress sanitization first
$filename = sanitize_file_name($filename);
// Must have .woff2 extension
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// No path components
if ($filename !== basename($filename)) {
return false;
}
// Reasonable length
if (strlen($filename) > 200) {
return false;
}
return $filename;
}
```
### Safe File Writing
```php
/**
* Safely write a font file to the fonts directory.
*
* @param string $filename Sanitized filename
* @param string $content File content
* @return string|WP_Error File path on success, WP_Error on failure
*/
function mlf_write_font_file($filename, $content) {
// Validate filename
$safe_filename = mlf_sanitize_font_filename($filename);
if ($safe_filename === false) {
return new WP_Error('invalid_filename', 'Invalid filename');
}
// Get fonts directory
$font_dir = wp_get_font_dir();
$destination = trailingslashit($font_dir['path']) . $safe_filename;
// Validate path
if (!mlf_validate_font_path($destination)) {
return new WP_Error('invalid_path', 'Invalid file path');
}
// Ensure directory exists
if (!wp_mkdir_p($font_dir['path'])) {
return new WP_Error('mkdir_failed', 'Could not create fonts directory');
}
// Write file
global $wp_filesystem;
if (empty($wp_filesystem)) {
require_once ABSPATH . 'wp-admin/includes/file.php';
WP_Filesystem();
}
if (!$wp_filesystem->put_contents($destination, $content, FS_CHMOD_FILE)) {
return new WP_Error('write_failed', 'Could not write font file');
}
return $destination;
}
```
### Safe File Deletion
```php
/**
* Safely delete a font file.
*
* @param string $path Full path to the file
* @return bool True on success, false on failure
*/
function mlf_delete_font_file($path) {
// Validate path is within fonts directory
if (!mlf_validate_font_path($path)) {
return false;
}
// Must be a .woff2 file
if (pathinfo($path, PATHINFO_EXTENSION) !== 'woff2') {
return false;
}
// File must exist
if (!file_exists($path)) {
return true; // Already gone, that's fine
}
return wp_delete_file($path);
}
```
---
## HTTP Request Security
### Outbound Requests (Google Fonts)
```php
$response = wp_remote_get($url, [
'timeout' => 15,
'sslverify' => true, // Always verify SSL
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
// Check for errors
if (is_wp_error($response)) {
// Log error, return gracefully
error_log('MLF: Google Fonts request failed - ' . $response->get_error_message());
return new WP_Error('request_failed', 'Could not connect to Google Fonts');
}
// Check HTTP status
$status = wp_remote_retrieve_response_code($response);
if ($status !== 200) {
return new WP_Error('http_error', 'Google Fonts returned status ' . $status);
}
// Get body
$body = wp_remote_retrieve_body($response);
if (empty($body)) {
return new WP_Error('empty_response', 'Empty response from Google Fonts');
}
```
### URL Validation (Google Fonts only)
```php
/**
* Validate that a URL is a legitimate Google Fonts URL.
*
* @param string $url URL to validate
* @return bool True if valid Google Fonts URL
*/
function mlf_is_valid_google_fonts_url($url) {
$parsed = wp_parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
// Only allow Google Fonts domains
$allowed_hosts = [
'fonts.googleapis.com',
'fonts.gstatic.com',
];
return in_array($parsed['host'], $allowed_hosts, true);
}
```
---
## AJAX Handler Complete Template
```php
'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_name = isset($_POST['font_name']) ? sanitize_text_field($_POST['font_name']) : '';
if (empty($font_name) || !preg_match('/^[a-zA-Z0-9\s\-]+$/', $font_name) || strlen($font_name) > 100) {
wp_send_json_error(['message' => 'Invalid font name']);
}
$weights = isset($_POST['weights']) ? array_map('absint', (array) $_POST['weights']) : [];
$weights = array_intersect($weights, [100, 200, 300, 400, 500, 600, 700, 800, 900]);
if (empty($weights)) {
wp_send_json_error(['message' => 'At least one weight required']);
}
$styles = isset($_POST['styles']) ? (array) $_POST['styles'] : [];
$styles = array_intersect($styles, ['normal', 'italic']);
if (empty($styles)) {
wp_send_json_error(['message' => 'At least one style required']);
}
// 4. PROCESS REQUEST
try {
$downloader = new MLF_Font_Downloader();
$result = $downloader->download($font_name, $weights, $styles);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success([
'message' => sprintf('Successfully installed %s', esc_html($font_name)),
'font_id' => $result,
]);
} catch (Exception $e) {
error_log('MLF Download Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
/**
* Handle font deletion AJAX request.
*/
public function handle_delete() {
// 1. NONCE CHECK
if (!check_ajax_referer('mlf_delete_font', 'nonce', false)) {
wp_send_json_error(['message' => 'Security check failed'], 403);
}
// 2. CAPABILITY CHECK
if (!current_user_can('edit_theme_options')) {
wp_send_json_error(['message' => 'Unauthorized'], 403);
}
// 3. INPUT VALIDATION
$font_id = isset($_POST['font_id']) ? absint($_POST['font_id']) : 0;
if ($font_id < 1) {
wp_send_json_error(['message' => 'Invalid font ID']);
}
// Verify font exists and is ours
$font = get_post($font_id);
if (!$font || $font->post_type !== 'wp_font_family') {
wp_send_json_error(['message' => 'Font not found']);
}
if (get_post_meta($font_id, '_mlf_imported', true) !== '1') {
wp_send_json_error(['message' => 'Cannot delete theme fonts']);
}
// 4. PROCESS REQUEST
try {
$registry = new MLF_Font_Registry();
$result = $registry->delete_font($font_id);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['message' => 'Font deleted successfully']);
} catch (Exception $e) {
error_log('MLF Delete Error: ' . $e->getMessage());
wp_send_json_error(['message' => 'An unexpected error occurred']);
}
}
}
```
---
## Security Checklist
Before committing any code:
- [ ] ABSPATH check at top of every PHP file
- [ ] index.php exists in every directory
- [ ] All AJAX handlers verify nonce first
- [ ] All AJAX handlers check capability second
- [ ] All user input sanitized with appropriate function
- [ ] All user input validated against allowlists where applicable
- [ ] All output escaped with appropriate function
- [ ] File paths validated to prevent traversal
- [ ] No direct SQL queries (use WordPress APIs)
- [ ] No `unserialize()` on user input
- [ ] No `eval()` or similar dynamic execution
- [ ] External URLs validated before use
- [ ] Error messages don't expose sensitive info