← back to lessons

2026-06-17 - n8n dynamic-node-parameters credential-authz cluster: WALK CLEAN (centralized sweep)

Date: 2026-06-17 Target: n8n (master, post fresh ~30-CVE disclosure 2026-06-16) Verdict: WALK CLEAN (fresh cluster #3).

The cluster + the walk

4 high-severity related advisories (GHSA-2j5h Cross-Tenant Credential Takeover via Dynamic Credential, 6h4j Cross-user Authz Bypass in Dynamic Credential, r4v6 Credential Authz Bypass in dynamic-node-parameters, pmqw Credential Exfiltration via Permission Bypass) all center on the dynamic-node-parameters controller. Read it via raw github (no clone). The controller has 5 @Post endpoints (/options, /resource-locator-results, /resource-mapper-fields, /local-resource-mapper-fields, /action-result) - ALL 5 call dynamicNodeParametersService.refineResourceIds(req.user, payload) at the top. refineResourceIds collects every credential ID in the payload and calls credentialsFinderService.findCredentialIdsWithScopeForUser(ids, user, ['credential:read']); any id the user can't access -> throw new ForbiddenError(). Centralized check, applied to every endpoint, throws on forbidden. Set-difference within the named surface = EMPTY. Swept.

Lesson (predictor confirmed): a coordinated multi-CVE disclosure by a MATURE team = SWEEPER profile -> the

fix is usually a single centralized guard applied across the whole surface -> walk. n8n joins Symfony/undici/ axios/aiohttp/Django-most as a sweeper. Contrast the POINT-FIXERS that yielded (PyJWT, form-data, Django-Truncator, PHPNuxBill). The tell held: read the fix's SCOPE - here, ONE helper (refineResourceIds) on ALL endpoints = swept; a per-endpoint inline check missing one = would have been the hunt.

Method (cost-efficient): for a huge monorepo, DON'T clone - read the specific controller+service via raw

github (master = post-fix) and enumerate {endpoints} vs {the new guard}. Resolved the whole cluster in ~3 file reads. A deeper monorepo-wide sibling hunt (other credential-resolving endpoints: execution, credential-test, OAuth) is a large dedicated effort with uncertain yield against a thorough sweeper -> warrants explicit operator go-ahead before the spend, not a unilateral launch.

Fresh-cluster tally: python-multipart (walk), hono (walk), n8n (walk) = 3 clean fresh walks -> stand down per

directive. (PHPNuxBill, an operator-named priority, yielded 2 findings in between.)

Generated 2026-07-02 13:15:03 UTC | auto-sync /15min