DEV Community

Piyush Gupta
Piyush Gupta

Posted on

Building a Community Plugin Marketplace for OWASP OWTF (GSoC 2026)

This is a write-up of what I built for Google Summer of Code 2026. I worked with the OWASP Foundation on OWTF (Offensive Web Testing Framework) over the summer. The rest of this post is basically: what the project was, why it needed to exist, how I built it, what went wrong along the way, and what I took away.

What I built

A Community Plugin Marketplace on top of OWTF. Any logged-in OWTF user could upload a Python plugin through the web UI. The plugin got validated on the server the moment it was uploaded, landed in a pending queue, and waited for an admin to look at the source. Once the admin approved it, the plugin got copied into OWTF's normal plugin table. After that, the runner, the worklist, and the report generator all treated it exactly like a built-in plugin.

Full delivery was six pull requests against owtf/owtf, around 6,000 lines of Python and TypeScript, and 153 backend unit tests. All of them are here if you want to look.

Why I built it

Before this summer, OWTF plugins had to live inside the OWTF repo. Nothing else. Which meant if you wrote a detection for some new attack, you had exactly two options: send a pull request to the main framework (slow, kind of intimidating), or just keep the plugin on your own laptop.

Both were bad. Contributing directly to a security framework is a real hurdle for most people, and in practice most useful community plugins were never going to make it upstream. A marketplace was the fix. Contributors upload, admins review, approved plugins run for everyone, and the framework grows without every good idea needing to go through the core maintainers.

How I built it

I split the work into six pull requests, one job each, so each PR could be reviewed and merged on its own timeline.

PR What it did
#1456 Data model plus an idempotent DB upgrader that ran on server start (no Alembic dependency)
#1457 AST validator for uploaded plugin source, with alias tracking
#1458 Admin role, JWT and @admin_required decorators, and an owtf-admin CLI
#1459 Manager plus REST endpoints (upload, list, mine, review, approve, reject)
#1460 Runner integration so approved plugins ran through OWTF's standard PluginRunner
#1461 React and TypeScript marketplace UI plus the trust model doc

The validator

The most interesting piece for me was the AST validator. It walked the source of an uploaded plugin before the file even hit disk, and it rejected:

  • Imports: os, sys, subprocess, socket, ctypes, signal, resource, pickle
  • Functions: eval, exec, compile, __import__, input
  • Dangerous calls: os.system, os.popen, subprocess(...) with shell=True
  • File writes: open(...) in write or append modes

A naive "just block subprocess" rule falls apart the second somebody aliases the import. So I made the validator track aliases too:

from subprocess import run as process
process("id", shell=True)   # still rejected
Enter fullscreen mode Exit fullscreen mode

Any name that resolved back to a blocked module or function got caught, even through renames and re-imports. Without that, the whole check would have been theatre.

The trust model

The AST validator was the safety gate at upload time, but the real security boundary of the marketplace was admin source code review. Approved plugins ran with the same permissions as built-in OWTF plugins. I wrote down exactly what that means, and what it does not mean, in docs/community_plugin_trust_model.md. It covers roles, endpoint exposure, serializer discipline, and what was intentionally left out of scope.

Challenges I faced

SQLite lied to me about foreign keys.

My first version of the runner integration synthesised community plugin keys at query time. All my local tests passed. All of them. And the moment the same code hit prod postgres, it crashed on a foreign key check. Turned out SQLite (my dev database) doesn't enforce FKs the same way PostgreSQL does, so the whole class of bug was invisible to my test suite.

The fix was to stop faking it. I changed the approval flow so it wrote a real "mirror row" into the standard plugins table with source = "community". Once that was in place, the FK to test_groups.code was respected, and every existing query, worklist call, and report generator just worked.

6,500 line pull requests are not pull requests.

I opened my first PR as one giant thing that covered everything: model, validator, admin role, API, runner, UI. Reviews were slow and shallow because nobody could hold that much context in their head at once, myself included. I split it into six focused PRs, and after that reviews got much faster, feedback got sharper, and each PR merged on its own schedule.

Splitting felt like starting over. It wasn't. It was just the actual work of shipping code that other people could review.

Serializer discipline is easy to get wrong.

Community plugin rows had three legitimate audiences: the public API, the plugin's owner, and admin reviewers. Each one got a different serializer. to_dict was public and only exposed metadata. to_owner_dict added rejection reasons for the uploader. to_admin_dict added reviewer identity and resource limits. file_path never left the server on any of them. I added a pytest assertion (TestSerializersNeverLeakFilePath) so any regression would fail loudly, because I knew otherwise I would forget.

What I learned

More than I expected. Things I'm taking into every future project:

  • Every hard bug is a design problem in disguise. The SQLite vs postgres crash wasn't a debugging problem, it was a "you never modelled the FK properly" problem. Fixing symptoms wasted time.
  • CI that runs against a different database than prod is not really CI. If your dev DB and prod DB diverge, set up a real prod-DB test run for anything that touches transactions or foreign keys.
  • Small PRs get reviewed. Large PRs get ignored. Splitting was the single highest-leverage thing I did all summer.
  • Write down the trust model. For anything security-related, the doc that spells out what you do and do not protect against is worth more than the code. It stops the next contributor from making the same mistakes.
  • Ask for real code review, not vibe checks. The reviews that actually changed my project were the ones where my mentor pushed back on design. Polite "looks good" reviews taught me nothing.

Gotchas for whoever picks this up next

  • Do not synthesise community plugin keys at query time. Persist the mirror row.
  • Plugin.code has a foreign key to test_groups.code. Use outer joins to keep community plugins visible in existing queries.
  • file_path must never leave the server. The pytest assertion mentioned above will yell if it ever does.
  • SIGALRM timeout enforcement only works on the main thread.
  • Import ordering matters: owtf/models/plugin.py has to import test_group before metadata.create_all(), or the FK breaks on fresh installs.

Out of scope and future work

A few things I deliberately left out of this summer's delivery:

  • Actual runtime isolation (containerised execution, seccomp). The trust boundary was admin review this summer. A future contributor could layer a container-per-plugin runner under the existing PluginRunner without breaking the API.
  • Plugin versioning. Approval was per upload. Supporting v1 to v2 upgrades on the same plugin name is the natural next step.
  • Community moderation (star, report, changelog). The data model has room for it, the UI just doesn't surface it yet.
  • Dependency review. The validator was static and didn't inspect third party packages a plugin might install.

Acknowledgements

Big thank you to my GSoC mentors for the reviews, the pushback, and the patience across the summer. Thanks also to the OWASP Foundation for accepting the project and to Google for running the program.

Links

Contact

If you're picking this project up after me, open an issue on owtf/owtf and tag @piyush140104. Happy to answer questions.

Top comments (0)