Skip to content

Add name squatting admin dashboard and exact match publish time refusal logic - #2081

Draft
autumnfound wants to merge 3 commits into
eclipse-openvsx:mainfrom
autumnfound:malowe/main/ovsxorg-1949
Draft

Add name squatting admin dashboard and exact match publish time refusal logic#2081
autumnfound wants to merge 3 commits into
eclipse-openvsx:mainfrom
autumnfound:malowe/main/ovsxorg-1949

Conversation

@autumnfound

Copy link
Copy Markdown
Contributor

This patch contains 2 new features. The first is a new admin dashboard tile for monitoring and addressing name_squatting flagged extensions + versions in a new view. This will allow for each of the versions to be resolved as false positive or to soft delete an extension if they are found to be malicious.

The second feature is a publish-time check that compares the display name to other active extensions outside of the users' namespaces. This does a case insensitive match on the display name, and if there is a match either for new extensions or a renamed new version of an extension, the upload gets rejected.

There is currently a small carve out in the second feature for currently existing extensions that collide as we cannot know which is the legitimate version and we don't want to immediately reject those uploads. It is assumed that the NAME_SQUATTING check results will be monitored and resolved to remove these conflicts as they are surfaced.

Resolves #1949.

@autumnfound
autumnfound marked this pull request as draft August 20, 2026 18:23
To best fulfill the requirements of an easy to use system to manage typosquatting, this PR was created. This leverages the existing NAME_SQUATTING publish check that uses levenshtein distance to check for similarly named extensions post-publish. The new dashboard view allows an admin to either mark a found check as a false positive and clear the check, or to soft-delete any extensions that are found to be maliciously typo squatting another extension. Any admin action taken will also be logged in the admin log table to make auditing these changes possible.

This check is left as unenforced as the check is too sensitive, and has many false positives. This does provide all of the features that were requested in the initial PRD for management of the system, outside of exclusion keywords.

Part of eclipse-openvsx#1949

Assisted-by: Claude Opus 5
…isplay name matches

To best accomadate the current infrastructure, a new check was added to the publish workflow. This check will compare the display name of new extensions and new versions to existing data. For new extensions, exact matches will be blocked from upload.

In the case of versions, extensions whose name changes to conflict will be blocked, but existing names will not be blocked. This carve out was added to address any existing conflict of display name without potentially blocking a legitimate version from being submitted.

This grandfathered clause makes the assumption that the name squatting results will be monitored and addressed to get to a known good state. Once we get to a clean state, the grandfathered clause will effectively work as a standard exact match gate without any notable performance impact.

Assisted-by: Claude Opus 5
@autumnfound
autumnfound force-pushed the malowe/main/ovsxorg-1949 branch from 0060c12 to 3337bed Compare August 20, 2026 18:24
@autumnfound

Copy link
Copy Markdown
Contributor Author

Report generated by Claude Opus 5 when requested to evaluate the performance with similar to production numbers for the impact of checking for a same-named extension with the new index.

TLDR: Actual lookup performance is very fast and should not be noticable in a running environment. We should consider doing some real world testing on staging to ensure that there is no significant impact with real data.

Performance: display name conflict check

What runs, and when

The check has two components. Both are gated so that a publication is only checked when it adopts a display name — i.e. when the extension is new, or when this version renames it.

┌───────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┐
│                     Component                     │                          Runs on                          │
├───────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ findActiveExtensionByDisplayName (conflict query) │ New extensions, and versions that change the display name │
├───────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ findLatestVersion (rename comparison)             │ Every publish of an existing extension                    │
└───────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┘

Both call sites — checkPublishPreconditions (scanning enabled only, fails fast before scanners are occupied) and doPublish (every publication) — apply the same gate.

Method

PostgreSQL 16.15; shared_buffers=2GB, effective_cache_size=6GB, work_mem=32MB, random_page_cost=1.1. Synthetic dataset shaped from current registry growth (~50–100 new extensions/day, ~450 new versions/day), carrying the full index set from V1_7, V1_10, V1_13, V1_22, V1_37 plus the new V1_72 index, and the personal_access_token / user_data / signature_key_pair tables that findLatestVersion joins.

Latency via pgbench -M prepared (server-side prepared statements, as pgjdbc uses), single connection, 10s per measurement, warm cache. Display names are held stable across an extension's releases, as they are in practice — so a name carried by N extensions matches N × (their version counts) rows in the index, not N rows. That is the realistic and less favourable shape.

┌─────────────────────────┬──────────────────────────┬──────────────────────────────┐
│                         │    Scale A (current)     │    Scale B (3× headroom)     │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Extensions              │ 20,000 (19,600 active)   │ 60,000 (58,800 active)       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Namespaces              │ 5,000                    │ 15,000                       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Versions                │ 470,000 (432,000 active) │ 1,410,000 (1,296,000 active) │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ extension_version table │ 71 MB                    │ 215 MB                       │
├─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│ Display-name index      │ 3.6 MB                   │ 11 MB                        │
└─────────────────────────┴──────────────────────────┴──────────────────────────────┘

Conflict query — findActiveExtensionByDisplayName

┌─────────────────────────────────────────┬────────────────────┬─────────────────┬──────────┬───────┬───────┬────────┬─────────────────┬──────────┬───────┬───────┬────────┐
│                Scenario                 │   Candidate rows   │ Scale A buffers │  A p50   │ A p95 │ A p99 │ A tps  │ Scale B buffers │  B p50   │ B p95 │ B p99 │ B tps  │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ No conflict                             │ 0 versions / 0 ext │ 3               │ 0.029 ms │ 0.044 │ 0.055 │ 25,561 │ 3               │ 0.030 ms │ 0.045 │ 0.060 │ 28,638 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 1 conflict                              │ 18 / 1             │ 82              │ 0.059 ms │ 0.099 │ 0.128 │ 12,477 │ 82              │ 0.058 ms │ 0.094 │ 0.114 │ 12,404 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 5 conflicts                             │ 100 / 5            │ 154             │ 0.078 ms │ 0.117 │ 0.141 │ 10,710 │ 153             │ 0.078 ms │ 0.137 │ 0.161 │ 10,673 │
├─────────────────────────────────────────┼────────────────────┼─────────────────┼──────────┼───────┼───────┼────────┼─────────────────┼──────────┼───────┼───────┼────────┤
│ 5 conflicts, all in excluded namespaces │ 100 / 5            │ 612             │ 0.189 ms │ 0.200 │ 0.233 │ 4,910  │ 612             │ 0.201 ms │ 0.281 │ 0.429 │ 4,890  │
└─────────────────────────────────────────┴────────────────────┴─────────────────┴──────────┴───────┴───────┴────────┴─────────────────┴──────────┴───────┴───────┴────────┘

Rename comparison — findLatestVersion

┌────────────────────────────────────────┬───────────────────────────────────┬─────────────────┬──────────┬───────┬───────┬─────────────────┬──────────┬───────┬───────┐
│                 Lookup                 │             Call site             │ Scale A buffers │  A p50   │ A p95 │ A p99 │ Scale B buffers │  B p50   │ B p95 │ B p99 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By entity                              │ doPublish, existing extension     │ 14              │ 0.033 ms │ 0.045 │ 0.074 │ 14              │ 0.034 ms │ 0.044 │ 0.059 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By namespace + name                    │ preconditions, existing extension │ 232             │ 0.102 ms │ 0.142 │ 0.198 │ 233             │ 0.098 ms │ 0.111 │ 0.165 │
├────────────────────────────────────────┼───────────────────────────────────┼─────────────────┼──────────┼───────┼───────┼─────────────────┼──────────┼───────┼───────┤
│ By namespace + name, no such extension │ preconditions, new extension      │ 19              │ 0.037 ms │ 0.050 │ 0.066 │ 20              │ 0.039 ms │ 0.065 │ 0.091 │
└────────────────────────────────────────┴───────────────────────────────────┴─────────────────┴──────────┴───────┴───────┴─────────────────┴──────────┴───────┴───────┘

Total per publication

Arithmetic sums of the Scale A p50 figures above, with scanning enabled so both call sites run:

┌──────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┬────────────┐
│               Publication                │                            Queries run                             │ Summed p50 │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ New extension, name free                 │ latest-by-name (null) + 2 × conflict query                         │ ~0.095 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ New extension, name taken                │ latest-by-name (null) + conflict query → rejected at preconditions │ ~0.096 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, name unchanged          │ latest-by-name + latest-by-entity, conflict query skipped          │ ~0.135 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, renamed to a free name  │ 2 × latest lookup + 2 × conflict query                             │ ~0.193 ms  │
├──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┼────────────┤
│ Further version, renamed to a taken name │ latest-by-name + conflict query → rejected at preconditions        │ ~0.161 ms  │
└──────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┴────────────┘

At ~100 new extensions and ~450 version publishes per day, that totals under 100 ms of database time per day. With scanning disabled only the doPublish call site runs, roughly halving each figure.

Verification

  • Zero sequential scans on extension, extension_version, or namespace in all 14 measurements. Every plan is index-driven.
  • Latency is flat across a 3× increase in data. Cost tracks how many extensions carry the queried name, not table size, so it does not drift as the registry grows.
  • The generic plan is identical. pgjdbc switches to a server-side generic plan after prepareThreshold=5; confirmed via pg_prepared_statements (generic_plans=5, custom_plans=5) that plan shape and buffer counts are unchanged. No degradation on the hot path.
  • No locks. Both queries are plain SELECTs — MVCC snapshot only.
  • The expression matches the index: jOOQ emits lower(TRIM(BOTH FROM display_name)), which PostgreSQL canonicalises to the indexed lower(btrim(display_name)), and the query's active = true matches the partial index predicate.

Reading notes

  • The "5 conflicts" row is not 5× the cost of one. LIMIT 1 exits at the first conflicting extension found (rows=1), so its cost depends on where in the index scan the first match falls, not on the total.
  • The all-excluded row is the true worst case for a given candidate count: every one of the 100 candidates must be evaluated because nothing can satisfy the query (rows=0), so there's no early exit. It's still ~0.2 ms. Given the same-namespace carve-out, high conflict counts for a single name shouldn't arise in practice.

Caveats

  • Synthetic data shaped from stated growth rates, not a production dump.
  • Single connection, warm cache, no concurrent write load.
  • signature_key_pair holds one row, so its join shows a sequential scan on a one-row table — cheap and realistic for a tiny table, but a synthetic-data artifact rather than a measured production plan.
  • The per-publication totals are sums of independently measured components, not end-to-end request measurements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update handling of similarly named extensions

1 participant