Add file-integrity check to protect the license gate from tampering

- New Integrity class checksums the files that implement/enforce the
  free/paid split (SubApps, Analytics, AnalyticsData, WhiteLabel, Shell,
  License itself, and the two bootstrap files) against a manifest
  generated at zip-build time. No manifest present (a dev checkout) is
  never treated as tampered.
- License::is_feature_available() checks Integrity::is_tampered() before
  checking the actual license, so a distributed copy with the check
  stripped/commented out re-locks every paid feature even with an
  otherwise-valid license, rather than the plugin trusting whatever code
  happens to still be there.
- bin/build-integrity-manifest.php generates the manifest against a
  staged zip-build copy (gitignored build artifact, same pattern as
  app/dist) — now a required step in the zip-build workflow.
- Verified live: untampered+unlicensed stays locked, untampered+licensed
  unlocks, one tampered file re-locks regardless of license status
  (tested via a temporary mu-plugin faking an active license, kept
  outside any protected file so it didn't itself trip the check).

This raises the bar against the realistic low-effort case (finding and
deleting/inverting the one license-check line) but can't stop someone who
rewrites a file's logic to never call the gate, or who edits this
integrity check itself — an inherent limit of protection living in plain,
editable PHP.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
nixpanel
2026-07-03 20:44:18 -05:00
co-authored by Claude Sonnet 5
parent 540083e6e4
commit cd66b70735
6 changed files with 222 additions and 18 deletions
+9
View File
@@ -77,12 +77,21 @@ class License
/**
* True for any feature not in PAID_FEATURES (nothing to gate) or when a
* license is active; false only when the feature is paid and unlicensed.
*
* Checked first, ahead of an actual license: if the gate files
* themselves have been tampered with (see Integrity::is_tampered()),
* every paid feature re-locks regardless of license status — a modified
* copy can't be redistributed showing paid features unlocked with no
* license at all.
*/
public static function is_feature_available($feature)
{
if (!isset(self::PAID_FEATURES[$feature])) {
return true;
}
if (Integrity::is_tampered()) {
return false;
}
return self::is_active();
}