initial commit
This commit is contained in:
parent
e468202f95
commit
423b9a25fb
24 changed files with 6670 additions and 0 deletions
621
native/wordpress/maple-icons-wp/SECURITY.md
Normal file
621
native/wordpress/maple-icons-wp/SECURITY.md
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
# SECURITY.md — Maple Local Fonts Security Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
This document covers all security requirements for the Maple Local Fonts plugin. Reference this when writing ANY PHP code.
|
||||
|
||||
---
|
||||
|
||||
## ABSPATH Check (Every PHP File)
|
||||
|
||||
Every PHP file MUST start with this check. No exceptions.
|
||||
|
||||
```php
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Silence is Golden Files
|
||||
|
||||
Create `index.php` in EVERY directory:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Silence is golden.
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
```
|
||||
|
||||
**Required locations:**
|
||||
- `/maple-local-fonts/index.php`
|
||||
- `/maple-local-fonts/includes/index.php`
|
||||
- `/maple-local-fonts/assets/index.php`
|
||||
- `/maple-local-fonts/languages/index.php`
|
||||
|
||||
---
|
||||
|
||||
## OWASP Compliance
|
||||
|
||||
### A1 - Injection Prevention
|
||||
|
||||
**SQL Injection:**
|
||||
```php
|
||||
// NEVER do this
|
||||
$wpdb->query("SELECT * FROM table WHERE id = " . $_POST['id']);
|
||||
|
||||
// ALWAYS do this
|
||||
$wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM %i WHERE id = %d",
|
||||
$table_name,
|
||||
absint($_POST['id'])
|
||||
));
|
||||
```
|
||||
|
||||
**Note:** This plugin should rarely need direct SQL. Use WordPress APIs (`get_posts`, `wp_insert_post`, etc.) which handle escaping internally.
|
||||
|
||||
### A2 - Authentication
|
||||
|
||||
All admin actions require capability check:
|
||||
|
||||
```php
|
||||
if (!current_user_can('edit_theme_options')) {
|
||||
wp_die('Unauthorized', 'Error', ['response' => 403]);
|
||||
}
|
||||
```
|
||||
|
||||
### A3 - Sensitive Data
|
||||
|
||||
- No API keys (Google Fonts CSS2 API is public)
|
||||
- No user credentials stored
|
||||
- No PII collected
|
||||
|
||||
### A5 - Broken Access Control
|
||||
|
||||
**Order of checks for ALL AJAX handlers:**
|
||||
|
||||
```php
|
||||
public function handle_ajax_action() {
|
||||
// 1. Nonce verification FIRST
|
||||
if (!check_ajax_referer('mlf_action_name', 'nonce', false)) {
|
||||
wp_send_json_error(['message' => 'Security check failed'], 403);
|
||||
}
|
||||
|
||||
// 2. Capability check SECOND
|
||||
if (!current_user_can('edit_theme_options')) {
|
||||
wp_send_json_error(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
// 3. Input validation THIRD
|
||||
// ... validate all inputs ...
|
||||
|
||||
// 4. Process request
|
||||
// ... actual logic ...
|
||||
}
|
||||
```
|
||||
|
||||
### A7 - Cross-Site Scripting (XSS)
|
||||
|
||||
**Escape ALL output:**
|
||||
|
||||
```php
|
||||
// HTML content
|
||||
echo esc_html($font_name);
|
||||
|
||||
// HTML attributes
|
||||
echo '<input value="' . esc_attr($font_name) . '">';
|
||||
|
||||
// URLs
|
||||
echo '<a href="' . esc_url($url) . '">';
|
||||
|
||||
// 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 '<div>' . $_POST['font_name'] . '</div>';
|
||||
|
||||
// RIGHT - sanitize input, escape output
|
||||
$font_name = sanitize_text_field($_POST['font_name']);
|
||||
echo '<div>' . esc_html($font_name) . '</div>';
|
||||
```
|
||||
|
||||
### 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
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MLF_Ajax_Handler {
|
||||
|
||||
public function __construct() {
|
||||
add_action('wp_ajax_mlf_download_font', [$this, 'handle_download']);
|
||||
add_action('wp_ajax_mlf_delete_font', [$this, 'handle_delete']);
|
||||
// NEVER add wp_ajax_nopriv_ - admin only functionality
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle font download AJAX request.
|
||||
*/
|
||||
public function handle_download() {
|
||||
// 1. NONCE CHECK
|
||||
if (!check_ajax_referer('mlf_download_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_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
|
||||
Loading…
Add table
Add a link
Reference in a new issue