Xgenious/ docs
Products
Get support

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.

Warning

⚠️ 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

LayerEnforced byWhat it prevents
ZIP path traversal guardPlatform (automatic)ZIP entries with ../ paths writing files outside the Modules directory
File extension whitelistPlatform (automatic)Executables, binaries, shell scripts, and other non-plugin file types inside a ZIP
Blocked path patternsPlatform (automatic)ZIPs containing .env, .htaccess, .git/, or wp-config entries
HTTPS enforcementPlatform (automatic)Non-encrypted update server URLs and download URLs (prevents MITM interception)
SHA256 checksum verificationPlatform + DeveloperTampered ZIPs delivered by a compromised CDN or MITM attacker
Install audit logPlatform (automatic)Undetected installs — every install/update is logged with IP, user, and checksum status
Input validationDeveloper responsibilitySQL injection, XSS, and CSRF in plugin-registered routes and controllers
Tenant data isolationDeveloper responsibilityOne 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

Note

ℹ️ 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:

PatternReason
/.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

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/
Warning

⚠️ 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 timeupdate_server is validated when plugin.json is loaded. A non-HTTPS URL will throw an InvalidPluginManifestException and prevent discovery.
  • Install timedownload_url is validated before the HTTP request is made. A non-HTTPS URL throws a PluginSecurityException and aborts the install.
Warning

⚠️ 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:

  1. Downloads the ZIP from download_url
  2. If sha256 was provided in the check response, computes hash('sha256', $zipContent) and compares with hash_equals() (timing-safe)
  3. Rejects the install if the hash does not match — logging a CRITICAL security event
  4. 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');
Warning

💡 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:

FunctionRiskAlternative
exec(), shell_exec(), system(), passthru()Arbitrary OS command executionLaravel Process facade (controlled args)
eval()Arbitrary PHP code executionRestructure logic to avoid dynamic evaluation
unserialize() on untrusted inputPHP object injection / RCEUse json_decode() instead
file_put_contents() with user-controlled pathArbitrary file writeValidate and whitelist paths explicitly
curl_exec() with user-controlled URLSSRF — internal network probingUse Laravel Http facade with a URL allowlist
extract() on $_REQUESTVariable injectionAlways 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.

ColumnDescription
plugin_idThe plugin's id from plugin.json
actioninstall or update
sourceupload (admin ZIP upload) or remote (update server pull)
versionThe version installed
from_versionPrevious version (only on update actions)
checksum_verifiedtrue only if sha256 was provided by the update server and matched
installed_byAdmin user ID who triggered the action
ip_addressIP address of the admin at install time
Warning

💡 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
Note

ℹ️ 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:

  1. Do not publish details publicly before the issue is resolved.
  2. Report to the platform maintainers via the contact listed in the marketplace or the GitHub repository.
  3. 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.
Still stuck?
Our support team is ready to help you get set up.
Get support