GSoC 2026 Proposal Draft - Idea 6: Strengthen the security of the plugin ecosystem --Sriram Varun

Title: GSoC 2026 Proposal Draft - Idea 6: Strengthen the Security of the Plugin Ecosystem - Sriram Varun Kumar


Links

Merged Pull Requests:

#14504 :- Mobile:Rich Text Editor: Fix extra blank line above nested lists
#14477:- Desktop: Fix UI freeze when closing plugin dialog with Escape key
#14421:-All: Fixes #14335: Support include_deleted parameter for GET /folders endpoint
#14461:-CLI: Fixes #13158: Fix null crash in e2ee decrypt command
#14541:-Desktop: Fixes #14196: Fix file:// links with backslashes for Windows UNC paths
#14580:-Mobile: Fix tapping rendered image scrolling to cursor position
#14559:-CLI: Fix trailing spaces in ls -l output
#14432:-Desktop: Fix "Copy dev mode command" producing an unquotable path on Windows

1. Introduction

My name is Sriram Varun Kumar, a B.Tech Computer Science student at Medhavi Skills University. I've been contributing to Joplin for the past few weeks and spent time reading through the plugin infrastructure specifically the plugin-repo-cli pipeline, the manifest validation logic, and how RepositoryApi.ts serves plugin data to the desktop app. Outside of Joplin I work with TypeScript, Node.js, and React, and I'm comfortable navigating large monorepos.


2. Project Summary

The problem:

The Problem:

Joplin has over 300 plugins today and every single one of them entered the plugin store the same way. an automated script detected it and it became available in the plugin store with no human ever reviewing it. No one looked at the code. No one checked the dependencies. The .jpl file that users download could contain anything and there is no way to verify it actually matches what is in the author's repository.

There are some safety nets in place. Plugins run in sandboxed processes, the manifest validation blocks ID hijacking, and a recommended flag lets maintainers highlight plugins they trust. But none of this touches the real problem. The binary users install is built privately on the author's own machine before Joplin's pipeline even sees it. A malicious author could show clean code on GitHub and publish something completely different to npm and nothing in the current system would catch it.

This problem has been discussed multiple times in the Joplin community:

  • LINK : Users have directly asked whether plugins in the official repository go through any security vetting

  • LINK : Some community members shared workarounds like manually reading source code before installing plugins or running a separate Joplin profile without plugins for sensitive notes

  • LINK : There was a proposal to sandbox plugins and restrict access to Node.js modules like fs, but it was considered too broad in scope at the time

  • LINK : There were concerns about what happens to a plugin's recommended status after updates, since no re-review takes place

Why It Matters to Users

Most Joplin users assume the official plugin store means some level of vetting has happened. That assumption is wrong. Every plugin installed today is a binary built on the author's own machine with no oversight from the Joplin team. For users who store sensitive notes or personal data in Joplin, this is a real risk.

What Will Be Implemented

Right now any plugin author can publish to npm and their plugin reaches every Joplin user with no one ever looking at the code. This project fixes that by building a review pipeline from the ground up.

The submission side is kept as simple as possible for plugin authors. They open a PR with their repository URL and commit hash, and a GitHub Action takes care of formatting the registry entry automatically. When the author is ready for review they comment "Review Plugin" and the CI does the heavy lifting, cloning the source, scanning dependencies, running a security analysis, and posting a full summary for the maintainer in one PR comment.

On the build side, once a maintainer approves and merges the PR, a trusted pipeline clones the plugin at the exact reviewed commit and builds the .jpl directly from source on Joplin's own CI runner, not the author's machine. The pipeline is split into two jobs with different permission levels so that even a malicious build script cannot write anything to the repository.

Every reviewed plugin will carry a permanent record of what it does, network access, filesystem usage, eval usage, and more, stamped directly into the manifest. Users will see a simple Reviewed or Unreviewed badge in the plugin manager.

The 300+ plugins already in the store are not left behind. A migration script tags them all as unreviewed while the existing flow keeps running in parallel so nothing breaks for current users. A security hold mechanism lets maintainers pull a dangerous plugin from distribution with a single field change, no deployment needed.

Expected Outcome

Every new plugin entering the store will be built from reviewed source code on a trusted server by Joplin's own CI, not the author's machine. Users will see a clear Reviewed or Unreviewed label in the plugin manager. Existing plugins remain installable but are honestly labelled. The ecosystem stays open to new plugins while becoming meaningfully more secure for the people who use it.

3. Technical Approach

Understanding Current System:

The

Recommendation System and Why It Falls Short

The _recommended field in PluginManifest is how Joplin surfaces trusted plugins to users. Recommended plugins appear more prominently in the plugin browser, so users naturally treat them as safer choices. But this flag only reflects a maintainer's choice to highlight a plugin. It does not mean the binary was ever built from reviewed source. A recommended plugin goes through the exact same unverified npm pipeline as any other, so the badge carries a level of trust the current system cannot actually back up.

The New System:

Architecture

The new system has two files with distinct roles.

approved-plugins.json - Pipeline Input

  • Stores the repository URL, reviewed commit hash, reviewer name, build command, and status for every plugin that has gone through review.

  • A plugin only gets built if its entry exists here and its status is approved.

manifests.json - Pipeline Output

  • The generated output that the Joplin app reads to list plugins.

  • It never gets modified directly. The build pipeline writes to it after a successful build.

Keeping them separate avoids a circular dependency where the pipeline reads from the file it also writes to. There was some discussion about consolidating them into manifests.json following the existing pattern of _recommended and _publish_hash. I think the separation is cleaner but I will discuss this with mentors during community bonding and go with whatever the team prefers.

How an Entry Is Created

When a plugin author opens a PR they provide their repository URL and the commit hash they want reviewed. The GitHub Action reads those two values and formats the full approved-plugins.json entry automatically, filling in the standard build command and publish directory. The reviewer name, review date, and approved status are then stamped onto the entry when the maintainer merges the PR. The pipeline reads the completed entry and only builds plugins where status is approved.

Forge Support

The system works with any public git forge that supports cloning at a commit hash. While the initial implementation targets GitHub, GitLab and Codeberg work with the same approach and need no architectural changes.

Figure 2: Sample approved-plugins.json Entry [POC]

Architecture

Changes to plugin-repo-cli

The core change is in packages/plugin-repo-cli/index.ts. The current commandBuild function searches npm and calls processNpmPackage for each result, downloading a pre-built .jpl that was built on the author's own machine. The new version reads approved-plugins.json and calls buildPluginFromSource for each approved entry.

buildPluginFromSource - Steps in Order

  1. Clones the repository at the exact reviewed commit hash

  2. Runs npm install with the --ignore-scripts flag to prevent malicious postinstall scripts

  3. Deletes the plugin's package-lock.json and regenerates it fresh from the real registry to prevent lockfile poisoning

  4. Runs the build command specified in approved-plugins.json

  5. Runs the ESLint security scan on the cloned source

  6. Stamps _security_signals and review metadata into the manifest

  7. Extracts the .jpl from the publish directory

If any step fails, the function exits immediately, cleans up the temporary directory, and reports the failure. No partial output is written.

Edge Cases

  • If a plugin's reviewed commit has not changed since the last build, the rebuild is skipped entirely to avoid unnecessary CI usage.

  • If a plugin specifies a custom build command in approved-plugins.json that differs from the standard npm run dist, that command is used instead, allowing plugins with non-standard build setups to work without being rejected.

What Stays the Same

The rest of the pipeline, writing manifests, updating the README, and creating GitHub releases, stays exactly the same. This minimises the change surface and avoids breaking anything that already works.

Submission Workflow

Plugin authors submit or update their plugin by opening a PR against joplin/plugins. A GitHub Action reads the repository URL and commit hash from the PR description and formats the approved-plugins.json entry automatically. The author never touches JSON directly.

What the PR Description Needs

  • Repository URL

  • Commit hash they want reviewed

  • Changelog or description of what changed (for updates)

Review Plugin Trigger

The CI does not start automatically on PR open. Instead a bot comments on every new PR asking the author to comment "Review Plugin" when they are ready. This keeps the review queue clean and avoids wasted CI runs on commits that are still being finalized.

The trigger has the following rules:

  • Only accepted from the PR author or a maintainer with write access to the repository

  • Comments from other users are ignored by the bot, preventing spam and unauthorised CI triggers

What CI Does After the Trigger

Once the author comments "Review Plugin" the CI runs the following and posts everything as a single summary comment for the maintainer:

  • Clones the repo at the submitted commit hash

  • Runs the dependency audit

  • Runs the ESLint security scan

  • Generates a diff from the last reviewed commit
    The system is designed around public git repositories. While the initial implementation targets GitHub, any public git forge that supports cloning at a commit hash such as GitLab or Codeberg is compatible with the same approach with no architectural changes needed.

Updates

For updates the author updates the commit hash in the PR description and comments "Review Plugin" again. The CI generates a diff between the old and new commits keeping the review focused and manageable.

Security Updates

For security updates, plugin authors report the vulnerability using GitHub Private Security Advisories following the instructions in SECURITY.md. This keeps the vulnerability details and the fix completely private until it is ready to ship. The maintainer collaborates with the author on the fix through the private advisory, reviews the patched commit manually, and only publishes once the fix is merged and distributed. Automated CI scanning does not trigger on private forks the same way it does on public PRs, so the maintainer reviews these manually. The tradeoff is acceptable since security fixes are typically small and targeted. This will be discussed further with mentors during community bonding to confirm the best approach.

Dependency Auditing and Change Detection

Dependencies are the biggest attack surface in any plugin. A plugin's own code might be 200 lines but its node_modules can contain thousands of packages. The CI handles this in three layers.

Layer 1: Template Diff on First Submission

On first submission the CI compares the plugin's api/, package.json, and webpack.config.js against the upstream Joplin plugin generator template and posts the diff in the PR comment. This surfaces anything unusual in files that might otherwise look like standard boilerplate and helps reviewers spot malicious changes without reading every line manually.

Layer 2: Lockfile Poisoning Prevention

Before running npm install the pipeline deletes the plugin's package-lock.json so npm regenerates it fresh from the real registry. This prevents lockfile poisoning where a manipulated lockfile could silently redirect npm to fetch trusted package names from an attacker controlled source. Both npm audit and ESLint would miss this attack since they only see package names not where they were fetched from.

Layer 3: Dependency Audit and Change Detection

The CI runs npm audit on the full dependency tree to catch known vulnerabilities. For updates it also compares the old and new package.json and posts a clear summary as a PR comment showing any added, removed, or version-changed packages. Reviewers immediately know if the dependency surface changed without having to dig through the diff themselves.

Edge Case

If a plugin has no package-lock.json in its repository the pipeline skips the deletion step and proceeds directly to npm install, generating a fresh lockfile from the real registry. If a plugin's repository contains a custom .npmrc pointing to a private registry, the pipeline flags this during the CI phase and blocks the submission until the author removes it, since the build environment cannot access private registries and the dependency source cannot be verified.

Out of Scope

Dependency allowlisting is out of scope for this project but is the natural next step once the review pipeline is running and patterns emerge about which packages are universally safe. That data can then be formalized into an allowlist, reducing reviewer burden on future submissions.

GitHub Actions CI Pipeline:

There are two workflows, both triggered by changes to approved-plugins.json.

Workflow 1: PR Check Workflow

Runs when the author comments "Review Plugin" on a submission PR.

  • Validates JSON format of the approved-plugins.json entry

  • Clones the plugin repo at the submitted commit hash

  • Deletes package-lock.json and regenerates it fresh from the real registry

  • Compares api/, package.json, and webpack.config.js against the upstream Joplin plugin generator template and includes the diff in the PR comment

  • Runs dependency audit and ESLint scan

  • Generates a diff from the last reviewed commit

  • Posts the full summary as a single PR comment for the maintainer

Workflow 2: Build-on-Merge Workflow

Runs when the PR is merged and the review is approved. This workflow is split into two separate jobs with different permission scopes.

Build Job - runs with contents: read permission only, never writes to the repository:

  • Clones the repo at the reviewed commit

  • Deletes package-lock.json and regenerates it fresh from the real registry

  • Runs npm install --ignore-scripts and the build command

  • Runs ESLint scan and stamps _security_signals into the manifest

  • Verifies the manifest ID matches the plugin ID in approved-plugins.json

  • Uploads the .jpl and manifest as a GitHub Actions artifact

Commit Job - runs after the build job with contents: write permission, never executes any plugin code:

  • Downloads the artifact from the build job

  • Commits the .jpl and manifest to the joplin/plugins repository

  • Creates the GitHub release

The needs: build line ensures the commit job only runs after the build job completes successfully. The build job has contents: read only and cannot write to the repository under any circumstance. The commit job has contents: write but never runs any plugin code. It only handles the artifact produced by the build job.

Figure 4: GitHub Actions Workflow - Separated Build and Commit Jobs

Why Job Separation is Necessary

Joplin plugins are built with Webpack and Webpack's own threat model states that it trusts project sources and assets. This means a malicious Webpack config could cause damage during the build step. By giving the build job no write permissions, a compromised build script cannot access the commit token or push anything to the repository. Only the commit job, which never runs plugin code, holds write access.

Edge Cases

If the build job fails after merge, the commit job does not run and no .jpl is published. The failure is visible in the GitHub Actions log and the maintainer can re-trigger the workflow manually once the issue is resolved.

Security Signals Scanning

The security scan runs as part of buildPluginFromSource before the .jpl is built. Since Joplin already uses ESLint this adds zero new infrastructure. No new service, no account, no Docker image.

ESLint Plugins Used

  • eslint-plugin-security for detecting eval, dynamic require, and unsafe patterns

  • @microsoftmicrosoftmicrosoftmicrosoft/eslint-plugin-sdl for detecting new Function and other dynamic execution vectors

  • A custom rule for detecting network calls like fetch, axios, and http.request

Sample PR Comment Output

The CI posts the following as part of the PR comment for the maintainer:

How Signals Are Used

  • The results are stamped into the manifest as _security_signals, a permanent field that every reviewed plugin carries

  • This data is available to the plugin manager and can be used by maintainers and future tooling

  • The user facing UI keeps it simple, showing just a Reviewed or Unreviewed badge

  • A plugin with network_calls: true and fs_access: true is not automatically rejected but a reviewer seeing both together will look more carefully at what the plugin is doing with both capabilities

Protection Against Self-Setting

validateUntrustedManifest.ts is updated to block plugin authors from self-setting _security_signals, following the same pattern already used for _recommended and _review_status. Only the build pipeline can stamp these fields onto a manifest.

Why Not Socket.dev

Socket.dev could serve as an enrichment layer for plugins still on npm during migration, but ESLint running on cloned source provides stronger guarantees with no external service dependency."

Optional Enhancement: Semgrep CE

If core deliverables land ahead of schedule, Semgrep CE can be added as a second layer on top of ESLint for deeper semantic analysis.

  • Fully open source under LGPL-2.1

  • Requires no account or API key

  • Runs directly on cloned source

  • This is explicitly a future enhancement as ESLint alone covers all the core signal

Manifest Extensions

PluginManifest in packages/lib/services/plugins/utils/types.ts is extended with the following new fields. These fields are pipeline-only, meaning only the build pipeline can write them. Plugin authors cannot set them manually.

New Fields and What They Mean

  • _review_status: either "reviewed" or "unreviewed". Set to "unreviewed" for all existing plugins during migration and set to "reviewed" only after a successful source build and human approval.

  • _reviewed_commit: the exact commit hash that was reviewed and built. This makes the review auditable. Anyone can check out this commit and verify what was reviewed.

  • _review_date: the date the review was completed. Stamped automatically on merge.

  • _security_signals: a permanent record of what the ESLint scan found in the plugin source. Contains eval_usage, child_process, dynamic_require, network_calls, fs_access, and scanned_at. This travels with the plugin forever and is available to the plugin manager and future tooling.

Figure 6: Extended PluginManifest Interface [POC]

Protection Against Self-Setting

validateUntrustedManifest.ts is updated to block plugin authors from self-setting any of these fields, following the same pattern already used for _recommended. Only the build pipeline can stamp these fields onto a manifest.

Figure 7: validateUntrustedManifest.ts Validation Check

Backward Compatibility

All new fields are optional, and the plugin manager treats a missing _review_status as unreviewed, so no existing plugin breaks.

Review Status in the Plugin Manager UI

The desktop app already shows _recommended badges in the plugin manager using an existing pattern in RepositoryApi.ts. The same pattern will be reused for review status, keeping the implementation minimal and consistent with what is already in the codebase.

Two States Shown to Users

  • Reviewed: source code reviewed by Joplin maintainers, built from source on a trusted CI runner, security signals scanned. The user can trust that what they are installing matches what was reviewed.

  • Unreviewed: entered through the npm-based flow, source not verified. The binary could have been built on the author's own machine. The user is informed and can decide.

What the Badge Shows

For reviewed plugins the badge will show the review date and a link to the reviewed commit so users can verify exactly what was audited. For unreviewed plugins the badge shows a clear warning that the source has not been verified.

Restricted Install Mode

In restricted install mode, only reviewed and recommended plugins would be installable. This gives organisations and privacy-conscious users a way to enforce a stricter plugin policy without modifying the app beyond a settings toggle.

Implementation Approach

  • The _review_status field in the manifest drives the badge display

  • RepositoryApi.ts is updated to read _review_status and pass it to the plugin info panel

  • The existing _recommended badge rendering is used as the reference implementation

  • No new UI framework or component library is needed

Vulnerability Reporting

A SECURITY.md will be added to joplin/plugins with clear instructions for reporting plugin vulnerabilities. The response mechanism reuses the existing manifestOverrides.json pattern keeping it consistent with what is already in the codebase.

Reporting Process

Plugin vulnerabilities are reported using GitHub Private Security Advisories following the instructions in SECURITY.md. This keeps the vulnerability details and the fix completely private until it is ready to ship. The process works as follows:

  • The reporter opens a private security advisory in the joplin/plugins repository

  • The maintainer is notified privately and coordinates with the plugin author on a fix

  • The author prepares a patched commit and shares it through the private advisory

  • The maintainer reviews the patched commit manually since automated CI does not trigger on private forks

  • Once the fix is ready the maintainer merges it and immediately applies a security hold if needed

  • The advisory is published only after the fix is distributed to users

This ensures the vulnerability is never publicized before users have access to the fix. The tradeoff of manual review for security fixes is acceptable since these are typically small and targeted changes. This will be discussed further with mentors during community bonding to confirm the best approach.

Immediate Response: Security Hold Mechanism

For a quick response, a _security_hold: true field in approved-plugins.json immediately pulls a dangerous plugin from distribution while a fix is in progress.

This follows the existing manifestOverrides.json pattern of using _obsolete: true with an _obsolete_reason, keeping the response mechanism consistent with what is already in the codebase.

  • A maintainer can pull a dangerous plugin from distribution with a single field change and one merged PR

  • No deployment required

  • The plugin remains visible in the plugin manager but with a clear security warning so users know not to install it

  • Once the fix is reviewed and a new build is published the security hold is lifted by removing the field

Full Response Timeline

  • Vulnerability reported privately via GitHub Security Advisory

  • Maintainer applies _security_hold immediately if the risk is active

  • Author prepares fix privately through the advisory

  • Maintainer reviews patched commit manually

  • Fix merged and new .jpl built and published through the normal pipeline

  • Security hold lifted and advisory published publicly

Migration

There are 300+ existing plugins that entered through the npm flow and cannot all be reviewed overnight. The three phase migration plan is shown in the diagram below.

One edge case worth noting - I checked the current manifests.json and found 8 plugins with no linked public repository. These cannot go through the source review flow since there is no code to review. They will remain tagged as unreviewed until the author links a repository and submits it for review.

Libraries and Technologies

  • TypeScript: all changes follow the existing codebase conventions

  • ESLint: eslint-plugin-s@microsoftmicrosoftcurity, @microsoft/eslint-plugin-sdl, and one custom rule for network call detection. Chosen because Joplin already uses ESLint, adding zero new infrastructure.

  • GitHub Actions: two workflow files, job separation using artifacts and permission scopes

  • GitHub Private Security Advisories: used for coordinating private vulnerability reports and fixes before public disclosure

  • zod: JSON schema validation for approved-plugins.json entries. Chosen over AJV because it gives TypeScript type inference directly from the schema with no separate type definitions needed.

  • Node.js 20: standardised across the build pipeline for consistency

Potential Challenges

Build environment consistency: Plugins may use different Node.js versions or custom build commands. The approved-plugins.json schema allows a build command per plugin and CI uses a standardised Node.js version to handle this.

Migration scale: Tagging 300+ plugins as unreviewed requires a migration script that runs without breaking existing installs. It will be tested against a full copy of manifests.json before production.

Review bandwidth: 300+ plugins is a large queue. The ESLint scan and dependency audit are designed to reduce manual effort per plugin so reviewers can focus on intent rather than pattern matching.

Plugins with native dependencies: Some plugins use native Node.js modules that require platform-specific compilation. These may fail in CI environments. Custom build commands can be specified in approved-plugins.json and the issue will be raised with mentors during community bonding.

Webpack configuration complexity: Joplin plugins use Webpack for bundling and some may have complex configs. As confirmed by Webpack's own threat model, Webpack trusts project sources, meaning a malicious config could behave unexpectedly. Job separation limits the blast radius since the build job has no write access, but complex configs may still cause build failures that need graceful handling.

Plugin authors not migrating: Some authors may not maintain their plugins or respond to review requests. These will remain permanently unreviewed and the UI will show a clear warning so users can decide.

Backward compatibility: The npm-based flow must keep running in parallel throughout GSoC without disruption. Any changes to plugin-repo-cli will be carefully tested to ensure the existing flow is unbroken while the new flow is built alongside it.

Lockfile poisoning edge cases: Deleting package-lock.json before every build solves the poisoning vector but introduces a new risk where npm resolves a slightly different version of a dependency than what the author tested. If a dependency releases a breaking update between the author's test and the CI build, the build fails unexpectedly. This will be handled by pinning dependency versions in package.json where possible and surfacing clear error messages when version resolution fails so the maintainer can investigate.

Private registry detection: Some plugins may have a .npmrc file pointing to a private or custom registry. When the pipeline regenerates the lockfile fresh, npm will fail to resolve packages from a registry it cannot access. The CI phase will check for the presence of a custom .npmrc and block the submission with a clear message asking the author to remove it before the pipeline can proceed.

Review turnaround under load: The automated CI posts everything a maintainer needs in one PR comment, keeping reviews fast even under load. If the queue grows, the ESLint scan, dependency audit, and template diff reduce time per review so no single plugin blocks the pipeline.

4.IMPLEMENTATION PLAN

Community Bonding: May 1 to May 24

The core PoC is already built - buildPluginFromSource, approved-plugins.json schema, JSON validation, and 7 passing tests. The bonding period will be used to align with mentors on the schema design and the approved-plugins.json vs manifests.json decision, discuss the job separation design for the build pipeline, confirm the GitHub Action auto-formatting approach for PR submissions, identify the top 10 most popular plugins to use as migration test cases, and refine the PoC based on mentor feedback.

Month 1: May 25 to June 20

Week 1-2:

  • Finalise approved-plugins.json schema based on mentor feedback

  • Build the GitHub Action that reads repo URL and commit hash from PR description and auto-formats the approved-plugins.json entry

  • Create PR submission template with "Review Plugin" comment trigger

  • Implement bot comment flow on PR open with trigger rules limiting it to PR author and maintainers only

  • Set up initial test data with 5-10 existing plugins

Week 3-4:

  • Integrate buildPluginFromSource into plugin-repo-cli/index.ts

  • Modify commandBuild to read from approved-plugins.json

  • Add lockfile deletion and regeneration step to buildPluginFromSource to prevent lockfile poisoning

  • Keep backward compatibility with existing npm flow during transition

  • Write tests for the new build path

Month 2: June 21 to July 18

Week 1-2:

  • Integrate npm audit into review pipeline

  • Build template diff between plugin's api/, package.json, and webpack.config.js against the upstream Joplin plugin generator template

  • Build dependency change detection between old and new package.json for updates

  • Write the PR check GitHub Actions workflow triggered by "Review Plugin" comment

  • Test with real plugin updates

Week 3-4:

  • Implement plugin-build.yml with separated build and commit jobs

  • Build job with contents: read only, outputs .jpl as artifact

  • Commit job picks up artifact, handles write step, never runs plugin code

  • Add manifest extensions including _security_signals, _review_status, _reviewed_commit, and _review_date

  • Integrate ESLint security scanning into buildPluginFromSource using eslint@microsoftmicrosoftplugin-security, @microsoft/eslint-plugin-sdl, and custom network call rule

  • Update validateUntrustedManifest.ts to block self-setting of all pipeline fields

  • End to end testing of full submission-to-build path

Midterm Evaluation: July 18 to July 25

Full pipeline working end to end. Plugin submitted via PR, GitHub Action auto-formats the entry, CI triggered by "Review Plugin" comment, dependency audit and ESLint scan posted as a single PR comment, maintainer approves, build job runs with contents: read only, commit job picks up artifact and publishes .jpl with _security_signals stamped into manifest. The midterm proof will be walking joplin-plugin-backup through the entire new flow.

Month 3: July 25 to August 16

Week 1-2:

  • Add review status display to plugin manager UI showing Reviewed and Unreviewed states

  • Update RepositoryApi.ts to read _review_status and pass it to the plugin info panel

  • Build migration script to generate approved-plugins.json from existing manifests.json

  • Mark all existing 300+ plugins as _review_status: "unreviewed"

Week 3-4:

  • Create SECURITY.md for joplin/plugins with vulnerability reporting instructions

  • Implement _security_hold mechanism in approved-plugins.json for rapid response

  • Write documentation for plugin authors on the new PR-based submission process

  • Write documentation for reviewers on reading the automated PR comment and handling security holds

  • If core deliverables are complete: begin Semgrep CE integration as optional second scanning layer

Final Assessment: August 17 to August 24

  • Comprehensive testing of the full workflow

  • Final bug fixes

  • Migration of at least 10 popular plugins to the new source-reviewed flow as proof of concept

  • Clean up PRs and ensure all code is well documented

5.Deliverables

Code

  • buildPluginFromSource function in plugin-repo-cli replacing extractPluginFilesFromPackage

  • Lockfile deletion and regeneration step inside buildPluginFromSource to prevent lockfile poisoning

  • GitHub Action that auto-formats approved-plugins.json entry from repo URL and commit hash in PR description

  • approved-plugins.json registry format with JSON schema validation via zod

  • PR submission template with "Review Plugin" comment trigger, restricted to PR author and maintainers only

  • Two GitHub Actions workflows: PR check workflow and build-on-merge workflow

  • Build-on-merge workflow split into two jobs: build job with contents: read only outputting an artifact, and commit job that picks up the artifact and handles the write step without ever running plugin code

  • Template diff of api/, package.json, and webpack.config.js against the upstream Joplin plugin generator on first submission

  • ESLint security scanning using eslint-plugin-security, @microsoft/eslint-plugin-sdl, and a custom network call rule, with _security_signals stamped permanently into manifests

  • validateUntrustedManifest.ts updated to block self-setting of all pipeline fields: _review_status, _reviewed_commit, _review_date, and _security_signals

  • Migration script to tag all 300+ existing plugins as _review_status: "unreviewed"

  • _security_hold mechanism in approved-plugins.json for rapid vulnerability response

  • SECURITY.md for joplin/plugins with clear vulnerability reporting instructions

  • Review status badges in plugin manager UI showing Reviewed and Unreviewed states

Tests

  • Unit tests for buildPluginFromSource

  • Integration tests for the full submission-to-build path

  • Regression tests confirming the existing npm-based flow is unbroken during transition

Documentation

  • Plugin author guide for the new PR-based submission process including the "Review Plugin" trigger

  • Reviewer guide for reading the automated PR comment, understanding ESLint signals, and handling security holds

  • Security response guide for the _security_hold mechanism

  • Updated README for joplin/plugins

6.Availability

  • 40 hours per week throughout the GSoC period

  • Timezone: IST (GMT+5:30), available 10am to 12 am IST

  • No exams, internships, or other commitments during the program

  • Will post weekly progress updates on the Joplin forum and remain active on Discord

  • Comfortable working asynchronously with mentors in different timezones

AI Disclosure

AI was used to correct grammatical mistakes, improve clarity and wording

2 Likes

Couple of questions (these are just some initial thoughts that I had reading the proposal, don't take anything said here as an actual requirement).

  1. Why a separate approved-plugins.json rather than modifying manifests.json with the additional fields (as we do for recommended as well as storage of the commit and publish hashes)?

  2. What does the status field in your approved-plugins.json file do? My understanding of the proposal (apologies if I missed something) was that the entry only gets added to that file once it has been reviewed and merged by the PR process? So why would this be anything other than approved?

  3. Section 9 also shows a "Pending" label to show in "untrusted mode" within the Joplin plugin manager UI. I assume this is for new and updated plugins and not the legacy ones which will be tagged as "unreviewed". I'm not sure I understand how this "Pending" state is achieved as my (basic) understanding of the process is:

  • Developer updates plugin code + creates PR to the Joplin plugin repo
  • Maintainers review code and merge or reject. The "review"
  • On merge the CI runs and updates approved-plugins.json which will build the artifacts from the original repository
  • .jpl and manifest added to repo (assuming manifests.json is updated as well)
  • Plugin is now discoverable as "reviewed"
  1. Lastly, the PR process to the repo seems to just be sending a commit hash and a URL for submission. I don't know how granular the review process wishes to be at this stage (i.e. just rejecting for actively malicious code or pointing out vulnerabilities that could be patched out) but that process has opportunity for human error. Is there potentially scope for performing the review on the same repo as the PR? i.e. the PR actually submits the repo code. That means the inbuilt GH review tools can be used which already can detect if parts of the code have been altered after an initial review or can ask for additional reviewer help if needed on specific lines. Although I'm aware this may involve alteration to the plugin template generator or to the repo/new repo branch etc. etc.
1 Like

@Daeraxa Thank You for reviewing! I have answered to each question, please check it out and suggest if any changes are required

Question 1

The reason I went with a separate file was to keep the review registry decoupled from the published manifest data, since they serve different purposes one is the trust anchor for the build pipeline, the other is what the joplin client reads to list plugins. But yeah you are right that extending manifests.json with the additional fields like the way _recommended and _publish_hash already work would be more consistent with the existing pattern. I am open to dropping the separate file entirely if that's the preferred direction

Question 2

The status field does real work in two cases: during migration, all existing plugins need to be added to approved-plugins.json as “unreviewed” so the build pipeline can still process them without falsely marking them as reviewed. It also handles incident response like if a previously approved plugin is found to have a security issue, you can flip the status to “suspended” to pull it from the build without deleting the entry entirely.

Question 3

The "Pending" label was meant to represent plugins with an open PR that has not been merged yet but making the plugin manager aware of open PRs adds unnecessary complexity. I will simplify this to just two states reviewed and unreviewed and discuss with mentors whether pending is actually worth adding

Question 4

It sounds interesting and my current approach keeps everything inside joplin/plugins so maintainers don't have to jump between external repos; the CI already generates the diff, so surfacing that directly in the PR and letting github's inline review tools handle line level discussion would be more powerful than what I have proposed. It would close the human error gap you are pointing at but the main tradeoff is that it might require changes to the plugin template generator and the repo structure which adds workflow complexity. I hadnot thought about it this way and it's worth discussing with mentors, i will be happy to explore
But it seems like out of scope for GSoC

1 Like

Thanks for answering, there were no right or wrong answers here or anything, just trying to understand some of your thought processes behind your decisions is all.

1 Like

@Daeraxa While exploring the github actions side of this proposal, I came across something that I think addresses part of your concern. I have updated the proposal to embed ESLint security scanning directly into the build pipeline specifically eslint-plugin-security, @microsoft/eslint-plugin-sdl, and a custom network call rule that runs automatically on the cloned source before the .jpl is built. The results get stamped permanently into the manifest as _security_signals

This doesnot fully replace inline code review but it does close the human error gap you're pointing at for the most dangerous patterns eval, child_process, dynamic require, external network calls, and filesystem access get flagged automatically before a human ever looks at the code. The reviewer then sees the ESLint report alongside the dependency diff in the PR comment, so the manual review is focused on intent and logic rather than pattern-matching for dangerous calls

Can you share your thoughts on this?

Hi @personalizedrefriger , @CalebJohn I have been working on the proposal for Idea 6 and have updated it based on feedback from Daeraxa. Would really appreciate your thoughts when you get a chance
Thank you

Thank you for posting a draft!

What are the risks of processing untrusted code with buildPluginFromSource? Will there be a security boundary between buildPluginFromSource and the code that uploads/commits the build output? (Does there need to be?)

Thank you for raising this! it made me think more carefully about the threat model

although the plugin code is reviewed by the reviewers , there can still be a risk of npm run dist running partially trusted [since review is already done] on the same runner that commits output. yes there's a theoretical risk, but I think the actual risk level in this system is lower than it appears because buildPluginFromSource only runs after a maintainer has reviewed and approved the exact commit hash being built

Even to avoid that and to make it more secure, Iam thinking the right approach will be something like:

  1. Explicit minimum permissions on the build job so the token scope is as narrow as possible

  2. Network restriction during the build step to block outbound calls - the most likely exfiltration vector

  3. The manifest verification step already in the pipeline catches any tampering with the build output

I have full job separation with artifact handoff in my mind and it is the more rigorous solution and Iam not opposed to it but I want to discuss with you whether the threat model justifies the added complexity given that builds only run on maintainer approved commits. Does this feel like the right tradeoff or would you prefer the stricter boundary?

Assuming that the build is done with Webpack, it's probably best to separate the build and upload jobs. Webpack's threat model document states that it trusts "Project sources and assets".

https://socket.dev/ scans NPM packages and summarize code security risks (example: koa package). Could it make sense to use this tool as a part of the plugin review process?

  • Benefits:
    • Socket.dev exists and provides potentially-useful security information.
  • Drawbacks:
    • Plugins would still need to be published as NPM packages.
    • Joplin's plugin NPM publishing format would still need to change: Plugin sources would need to be published instead of JPL files.

So sorry for replying late, i was having an exam; Thank you for the suggestions

regarding full job separation i think now it’s actually necessary to keep it, thank you for linking webpack's threat model, i hadnot considered that webpack itself trusts project sources, which means a malicious webpack config could cause damage during the build step even if --ignore-scripts blocked postinstall scripts

I will update the proposal to separate the build and commit jobs explicitly , now the build job will have no write permissions and will output the .jpl as an artifact and the commit job picks it up and handles the write step without ever running plugin code

Regarding the socket.dev, i think that is a genuinely good tool and I can see why it would be useful here but I think it will conflicts with the core premise of this proposal. Socket.dev scans npm packages, which means plugins would still need to be published to npm and that's exactly what this proposal is trying to move away from and if plugins are published as source to npm instead of .jpl files, the npm publishing step remains in the chain and introduces the same verification gap we are trying to close

for source based scanning I think ESLint with eslint-plugin-security is the better fit because it runs directly on the cloned source code with no npm publishing required and then Semgrep CE can add a second layer on top for deeper semantic analysis. Both run in CI on the raw source and require no external service or npm publishing [ i will mention it as a optional/future enhancemnet]

That said if there's a version of Socket.dev integration that works without npm publishing I will be very interested to explore it I may be missing something about how it could fit

1 Like

Hi @CalebJohn would appreciate it if you could take a look on my proposal when you get the chance. Thank you.

I'm currently reading it now :slight_smile: nice coincidence.

1 Like

Thanks for the proposal, below are some comments

  • Please add a bit more description of what the new flow looks like for plugin authors. Specifically how will a plugin author open a PR with the correct format/changes. For reference the current flow for authors is to simply run npm publish at the command line.

This can still be considered, source code can be pushed to npm, and the reviewer would be able to look look at the socket.dev output. I'm not saying that needs to be the way, but it should be considered.

I think both approved-plugins.json and manifests.json are outputs? I also don't see the value in separating them.

Ignore scripts is a good idea. I wonder if we should go further and have a fixed build environment that is not copied from the plugin source. This would mean only the src/ folder is copied to the plugin repo and used by the builder. The Joplin plugin generator ships a standard builder currently, I know some plugins (including some of mine) edit this, but it could be possible to restrict that. Just a thought, it might be too onerous for the plugin devs.

thank you for the detailed feedback and let me address each point,

regarding author flow: yeah i agree that the proposal does not explain the author experience clearly enough. The flow would be: open a PR against joplin/plugins adding their entry to approved-plugins.json with their repository URL and the commit hash they want reviewed, the PR template will guide authors through the exact fields need, authors add their repository URL and commit hash to approved-plugins.json and open the PR and the template will include a format example so the process is straightforward even for first time contributors

On Socket.dev: I think I was too quick to rule it out , socket.dev could add value as an additional signal for plugins that are still on npm during the migration period, for plugins that have fully migrated to the source based flow, ESLint and Semgrep CE running on cloned source provide stronger guarantees without any external service dependency. I am open to including socket.dev output during the transition period if the team thinks it is useful

regarding approved-plugins.json & manifests.json: You make a fair point, to clarify how approved-plugins.json works in this design: the author adds their entry via PR with the repository URL, commit hash, and build command, the status field is only set to approved when the maintainer merges the PR. then the pipeline reads approved-plugins.json and only builds plugins where status is approved. The build output then gets written to manifests.json. So approved-plugins.json is the human controlled input that the pipeline reads from, and manifests.json is the machine generated output the pipeline writes to. merging them into one file would mean the pipeline is reading from and writing to the same file in the same run which creates a circular dependency, can you share your thoughts on this!

regarding fixed build environment, this is a really interesting idea, it is genuinely the stronger security model and I appreciate the suggestion. --ignore-scripts prevents postinstall scripts but the plugin's webpack config still runs during the build step which is a real gap, a fixed builder using only src/ would eliminate that entirely , that said the migration complexity concerns me since many existing plugins including some of yours customise their webpack config for legitimate reasons.

my thinking is to start with --ignore-scripts as the pragmatic baseline for GSoC since it already significantly reduces the attack surface compared to the current system, and treat the fixed builder approach as a clearly defined next step once the standard generator is more widely adopted across the plugin ecosystem. Please share your thoughts on this

Depending on how the build step is done (e.g. if the original package-lock.json is included or not), something else to consider here might be lockfile poisioning. In particular, a malicious plugin might have safe/trusted dependencies in package.json, but its package-lock.json could subtly direct NPM to fetch these dependencies from an unsafe, attacker-controlled source.

Thank you raising this

Both are really good points. for the template diff, i will add a CI step on first submission that compares api/, package.json, and webpack.config.js against the upstream Joplin plugin generator template and includes the diff in the PR comment , this will surfaces anything unusual in files that might otherwise look like standard boilerplate

for lockfile poisoning, your specific example makes the threat much clearer, something a poisoned package-lock.json that redirects npm to fetch trusted package names from an attacker controlled source would bypass both npm audit and ESLint completely since both only see the package names not where they were actually fetched from

the fix is to delete the plugin's package-lock.json before running npm install so npm always resolves fresh from the real registry using only package.json, this eliminates the attack vector entirely and the tradeoff is reproducibility but a build failure from a version change is visible and investigable. A silently poisoned lockfile is not.

I will add both of these to the dependency auditing section of the proposal.

I don’t feel this is a good UX for plugin developers, as I read it, this will involve a multi-step manual process for every update. As I said above, the current process is a single command in the terminal, you should strive to match or improve that UX.

I understand now, approved-plugin is the file that plugin developers edit to request a review, naturally it makes sense to keep separate. As noted above, I don’t think this is a good UX for plugin developers. This proposal should take care to keep the review process as streamlined as possible, but also keep the submission process streamlined.

yeah you are right and I take the point, to keep the submission process as simple as possible within the current scope, the PR template will do most of the heavy lifting.

To address the UX concern, i can look at automating the entry creation through a github action that runs when the author opens a PR, the author only provides their repository URL and commit hash in the PR description in plain text, no JSON editing required, then the Action reads those two values, formats the approved-plugins.json entry automatically, and commits it to the PR, now that keeps the submission down to two inputs and a PR open, which is much closer to a single command experience. This keeps the complexity inside CI rather than requiring a separate tool like CLI helper

A CLI helper that automates this further can be added after the pipeline is running but adding it within GSoC scope would stretch the timeline beyond what is already planned, i am open to discussion on this devs UX , can you share your thoughts on this?

@personalizedrefriger Can you share your thoughts on my approach solving UX concern