Security Overview
Because plugins run as first-class PHP code inside the Nazmart process, the platform applies a multi-layer security model covering ZIP installation, remote update distribution, runtime behaviour, and admin audit trails. This section documents every rule — both what the platform enforces automatically and what you as a developer are responsible for.
⚠️ Plugins have full Laravel app access. There is no sandbox. A plugin can read the database, write files, make outbound HTTP requests, and access .env values. Platform administrators trust installed plugins implicitly — treat that trust seriously.
Defence-in-Depth Layers
| Layer | Enforced by | What it prevents |
|---|---|---|
| ZIP path traversal guard | Platform (automatic) | ZIP entries with ../ paths writing files outside the Modules directory |
| File extension whitelist | Platform (automatic) | Executables, binaries, shell scripts, and other non-plugin file types inside a ZIP |
| Blocked path patterns | Platform (automatic) | ZIPs containing .env, .htaccess, .git/, or wp-config entries |
| HTTPS enforcement | Platform (automatic) | Non-encrypted update server URLs and download URLs (prevents MITM interception) |
| SHA256 checksum verification | Platform + Developer | Tampered ZIPs delivered by a compromised CDN or MITM attacker |
| Install audit log | Platform (automatic) | Undetected installs — every install/update is logged with IP, user, and checksum status |
| Input validation | Developer responsibility | SQL injection, XSS, and CSRF in plugin-registered routes and controllers |
| Tenant data isolation | Developer responsibility | One tenant reading or modifying another tenant's data via plugin endpoints |
ZIP Packaging Rules
Every plugin distributed as a ZIP — whether uploaded via the admin panel or installed via plugin:install — is scanned before any files are extracted. A single violation causes the entire installation to be rejected with a 422 error and a PluginSecurityException logged to storage/logs/plugin.log.
Allowed File Extensions
The platform maintains an explicit allowlist. Files with any other extension will block installation:
php json js css html blade svg png jpg / jpeg gif / webp ico woff / woff2 ttf / eot md / txt xml yaml / yml map / lock
ℹ️ If your plugin ships compiled assets (e.g. .wasm, .gz), contact platform support to have the extension reviewed for allowlisting. Do not attempt to rename files to bypass the check — the extension is read from the ZIP entry name.
Blocked File Patterns
Even if the extension is allowed, entries matching any of these patterns are rejected:
| Pattern | Reason |
|---|---|
/.env/ | Could overwrite application environment variables |
/.htaccess/ | Could modify Apache rewrite rules or access controls |
/wp-config/ | Could shadow WordPress config files on shared hosts |
/.git/ | Git metadata should never be distributed in a release ZIP |
Path Traversal Prevention
The platform resolves every ZIP entry path symbolically before extraction and verifies it stays inside the target Modules/ directory. These ZIP structures are all rejected:
# Rejected — classic path traversal
../../public/shell.php
# Rejected — null byte injection
plugin/malicious.php\x00.jpg
# Rejected — Windows-style traversal
plugin\..\..\..\bootstrap\app.php
Recommended ZIP Structure
Your ZIP must contain exactly one top-level directory whose name matches your plugin folder, with plugin.json at the root of that directory:
acme-seo-1.2.0.zip
└── AcmeSeo/ # top-level dir — matches Modules/ subdirectory
├── plugin.json # manifest — must be here
├── src/
│ └── AcmeSeoPlugin.php
├── resources/views/
└── database/migrations/
⚠️ Do not nest your plugin inside extra subdirectories. The scanner looks for [top-dir]/plugin.json. A structure like release/AcmeSeo/plugin.json will fail manifest discovery and the install will be rejected.
Update Server Contract
If your plugin declares an update_server, the platform polls {update_server}/info to check for new versions and downloads from the URL you return. Both the server URL and the download URL are subject to strict security requirements.
HTTPS Requirement
The platform enforces HTTPS at two points:
- Manifest parse time —
update_serveris validated whenplugin.jsonis loaded. A non-HTTPS URL will throw anInvalidPluginManifestExceptionand prevent discovery. - Install time —
download_urlis validated before the HTTP request is made. A non-HTTPS URL throws aPluginSecurityExceptionand aborts the install.
⚠️ HTTP is rejected, not downgraded. There is no automatic upgrade to HTTPS. Using http:// in either field will cause a hard error.
SHA256 Checksum
The platform supports ZIP integrity verification via SHA256. Including a checksum in your update server response is strongly recommended — it is the only protection against a tampered download if your CDN or delivery network is compromised.
Update Server Response Format
// GET https://updates.acme.com/info?plugin_id=acme-seo&version=1.1.0
{
"version": "1.2.0",
"changelog": "Bug fixes and performance improvements.",
"download_url": "https://cdn.acme.com/releases/acme-seo-1.2.0.zip",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
When the admin clicks Update, the platform:
- Downloads the ZIP from
download_url - If
sha256was provided in the check response, computeshash('sha256', $zipContent)and compares withhash_equals()(timing-safe) - Rejects the install if the hash does not match — logging a
CRITICALsecurity event - Only if the hash passes (or no hash was provided), proceeds to ZIP extraction
Generating a SHA256 for Your Release
# Linux / macOS
sha256sum acme-seo-1.2.0.zip
# → e3b0c44298fc1c14... acme-seo-1.2.0.zip
# macOS shasum
shasum -a 256 acme-seo-1.2.0.zip
# PHP (in your release pipeline)
echo hash_file('sha256', 'acme-seo-1.2.0.zip');
💡 Store the SHA256 hash alongside the release artifact (e.g. in your release database or JSON manifest), not computed on-the-fly from the live file. If your CDN serves a cached version that differs from the current file, on-the-fly hashing will produce an inconsistent value.
Audit Log Entry for Remote Updates
Every successful remote update writes a row to plugin_install_log. The checksum_verified column will be true only when a sha256 was provided and matched. Admins can review this in the Install Log tab of the Plugins admin page — a shield icon indicates verified installs.
Secure Code Practices
The platform enforces ZIP-level and transport-level security automatically. The following rules are your responsibility as a plugin developer.
Input Validation & SQL Injection
All user input entering your plugin via routes or hooks must be validated. Use Laravel's built-in tools:
// ✅ Good — validated and bound parameters
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'name' => 'required|string|max:120',
'score' => 'required|integer|between:1,5',
]);
DB::table('acme_reviews')->insert($data); // bound — safe
}
// ❌ Bad — raw user input in query
DB::statement("INSERT INTO acme_reviews (name) VALUES ('" . $request->input('name') . "')");
XSS Prevention
Never output raw user data in Blade templates. Use the double-brace syntax which HTML-encodes automatically:
{{-- ✅ Safe — HTML-encoded --}}
{{ $review->name }}
{{-- ❌ Unsafe — raw output --}}
{!! $review->name !!}
Use {!! ... !!} only for trusted HTML you generated yourself (e.g. a rendered Blade partial or a Markdown-to-HTML library with an allowlist).
CSRF Protection
All plugin routes registered under the web middleware group inherit Laravel's CSRF protection. Ensure every state-changing form includes the token:
<form method="POST" action="{{ route('acme.reviews.store') }}">
@csrf
<!-- fields -->
</form>
Plugin API routes registered via register_api_routes() use the api middleware group (no CSRF — use token/Sanctum auth instead).
Tenant Data Isolation
In type: "tenant" or type: "both" plugins, every database query that touches tenant-specific data must be scoped to the current tenant. Never trust a tenant ID from user input for cross-tenant queries:
// ✅ Good — scoped to the authenticated tenant
$tenantId = tenant()->id;
DB::table('acme_reviews')
->where('tenant_id', $tenantId)
->get();
// ❌ Bad — tenant_id from request input allows cross-tenant data access
DB::table('acme_reviews')
->where('tenant_id', $request->input('tenant_id'))
->get();
Sensitive Configuration
Never hardcode secrets (API keys, webhook secrets) in plugin PHP files or plugin.json. Store them via the Settings API and let the platform admin enter them:
// ✅ Good — admin-entered, stored in plugin_options
$apiKey = $this->get_option('acme_api_key');
// ❌ Bad — hardcoded credential visible in source
$apiKey = 'sk_live_abc123xyz';
Dangerous PHP Functions
The following PHP functions should never appear in a plugin that will be distributed to customers. Their presence will cause rejection during marketplace review:
| Function | Risk | Alternative |
|---|---|---|
exec(), shell_exec(), system(), passthru() | Arbitrary OS command execution | Laravel Process facade (controlled args) |
eval() | Arbitrary PHP code execution | Restructure logic to avoid dynamic evaluation |
unserialize() on untrusted input | PHP object injection / RCE | Use json_decode() instead |
file_put_contents() with user-controlled path | Arbitrary file write | Validate and whitelist paths explicitly |
curl_exec() with user-controlled URL | SSRF — internal network probing | Use Laravel Http facade with a URL allowlist |
extract() on $_REQUEST | Variable injection | Always name variables explicitly |
Outbound HTTP Requests (SSRF)
If your plugin makes outbound requests based on user-supplied URLs, validate the target before connecting:
use Illuminate\Support\Facades\Http;
// ✅ Good — only allowed domains
$allowed = ['api.stripe.com', 'api.paypal.com'];
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowed, true)) {
throw new \InvalidArgumentException('URL not allowed');
}
Http::get($url);
// ❌ Bad — blindly follows user input, allows internal network requests
Http::get($request->input('webhook_url'));
Audit & Monitoring
The platform provides two admin-facing audit tools for plugin activity. As a plugin developer, understanding what is logged helps you debug installs and demonstrates to customers that operations are traceable.
Install Log (plugin_install_log)
Every install and update writes a row to this table. It is visible at Admin → Plugins → Install Log.
| Column | Description |
|---|---|
plugin_id | The plugin's id from plugin.json |
action | install or update |
source | upload (admin ZIP upload) or remote (update server pull) |
version | The version installed |
from_version | Previous version (only on update actions) |
checksum_verified | true only if sha256 was provided by the update server and matched |
installed_by | Admin user ID who triggered the action |
ip_address | IP address of the admin at install time |
💡 If your update server provides a sha256 in its response, the Install Log will show a green shield icon for that install. Customers can use this to confirm the installed ZIP was not tampered with.
Hook Log (plugin_hook_log)
Hook executions that exceed the configured duration threshold are recorded. This is primarily a performance tool, but it also provides a forensic trace of which hooks fired and which plugins handled them. Visible at Admin → Plugins → Hook Log.
Plugin Log Channel
All plugin system events — boot errors, security violations, update failures — are written to the dedicated plugin log channel:
# Live-tail plugin events
tail -f storage/logs/plugin.log
# Security-level events are logged as CRITICAL
# Example entry for a failed SHA256 check:
# [2026-04-30 12:00:00] plugin.CRITICAL: SECURITY: Plugin security check failed:
# SHA256 mismatch for [acme-seo] — expected [abc...], got [def...]
Security Event Escalation
The following events are logged at CRITICAL level and appear prominently in any log aggregation tool (Papertrail, Datadog, etc.):
- ZIP path traversal attempt detected
- Blocked file type found in ZIP (
.exe,.sh, etc.) - Blocked path pattern found in ZIP (
.env,.git, etc.) - Non-HTTPS download URL supplied
- SHA256 checksum mismatch on a remote update
ℹ️ If you are integrating with a log aggregation service, set up an alert on CRITICAL entries in the plugin channel. These events indicate either a misconfigured update server or an active attack attempt and warrant immediate investigation.
Responsible Disclosure
If you discover a security vulnerability in the Nazmart plugin system or in a distributed plugin, please follow responsible disclosure:
- Do not publish details publicly before the issue is resolved.
- Report to the platform maintainers via the contact listed in the marketplace or the GitHub repository.
- For vulnerabilities in your own plugin, push a patched release to your update server immediately and increment the version in
plugin.json. Platform installations will detect the update on the next scheduled check.

