Repository navigation
First-class tagging for JavaScript frontend frameworks - #201
Merged
Merged
Conversation
…recision fixes (work in progress)
…ridge (work in progress)
…orm actions (work in progress)
…red origins (work in progress)
…ixes (work in progress)
…; URL-part request sinks - Angular: resolve calls into the project's own injected services to the service's method; httpResource URLs, reactive form valueChanges, host metadata window listeners and Router.navigateByUrl. - A request sink is the part of the URL that decides the target (whole URL, variable base, path variable), never the request call, so bodies and options are no forgery; a URL that is configuration (this.host, SCREAMING_CASE, environment.*) is none. Package attribution follows the URL part to its request. - Web Storage reads are service-ingress (stored data, like a response). - pages/api handlers: only req is input. - Controller-file heuristic: in a frontend repo carrying a Node server, a matching file that makes requests or imports a frontend framework is a client, not a controller.
…om the base use
The nested-container rule linked an inner call to the access on its result
(route.query -> route.query.next) but not an inner identifier, so a read like
data.motd - whose base is a plain identifier - had no incoming edge at all and a
walk starting at it (the {@html data.motd} sink) found nothing.
…same-run call sites ReactiveBridge tags the derivation call itself when its callback returns a source - a composable that returns computed(...)/useState(key, fn) hands the taint to every caller through its return value; useState joined the derivations with its first function argument as the callback. MemberTaintBridge learned captured local containers (term.value = v, q.set(v)): the container's value crosses methods only through this bridge, like the class fields it already handled. sourceTagThrough follows a parameter to the arguments its callers pass, so a bridge can decide taintedness through one resolved call; JsTagger records the CALL edges it resolves because they land in the graph only when the pass ends.
…ted members as predicates
useLoaderData/useActionData resolve to the loader their type argument names
(or the file's single data function); a route component's $props() resolves to
the sibling load; a transformPageChunk callback's return value is an HTML sink;
runTask('db:reindex') resolves to the task file's run method; inject(key)
resolves to the callback behind provide(key, computed(...)) with keys
normalised to project paths; pinia store hooks resolve to their setup and their
method calls to the functions it returns; a call to a project function that
returns a derivation resolves to it. A validateSearch schema's non-text fields
(zod/valibot/yup number, enum, boolean, literal) make reads of those members of
useSearch() results predicates - nothing to inject - while z.string()
neutralises nothing. SvelteKit handlers' request members are tagged where they
are read, and useLocation().state reads are sources.
An element whose name is a capitalized identifier (<RichText html={bio} />)
is a component reference; the element now also lowers to a CALL named after
it, with the element's first computed attribute value as its argument. The
attribute expressions are lowered a second time for the call (constants and
boolean attributes carry nothing); the originals stay under the attribute
template nodes where the template sinks are tagged. ComponentCallBridge
resolves the call to the component - the import that bound the name, or the
one project method of that name - so a parent's value reaches the
component's props parameter, which is its only input boundary. A component
that is a FILE (.vue/.svelte) has no parameter to receive the argument and
stays unresolved; its props-side flow starts at the defineProps()/$props()
read.
…efix
Alpine's x-html="expr" interpolates without escaping like v-html and
{@html}; x-bind:href names the href binding the way v-bind: and : do.
…ngs read the inputs An Angular component's x.component.html is linked to its component by the file convention. [(ngModel)]="field" means the control writes the field with user input, so the field's this-reads are sources; a binding that names an input (caption(), partner()) reads the component's input. An HttpClient request whose URL was built into a local keeps the local's origin shape, so a host the service configures is not a request sink; a template attribute's name strips an Angular property-binding bracket.
…ts and html templates as lowerings; validation as a neutraliser
…work Component elements: the lowered call carries one props object, <operator>.jsxProps over every value the element computes (it carried only the first attribute, so a tainted second prop was lost and a tainted first one stood for all). ComponentCallBridge finds element calls by their element child, resolves only function components in script files - a default import to the module's default export, a named one to its export - instead of the first anonymous method of whatever file matched, and resolves extension-qualified imports. Template interpolations are wrapped even when the value is a call, so a signal read (`partner()`, `props.html`) can be the source and the interpolation the sink. Angular templates: linked by @component templateUrl, not by a filename regex. Property bindings keep their brackets and are no markup sinks - Angular sanitises them; only [href]/[attr.href] on <a>/<area> is a link sink. Pipes resolve to the @pipe class's transform, so a bypassing pipe is reached through its own code; the member bridge also tags a component's template reads. Provenance: bridges tag what they make sources with via:<file>#<line> - the value they carried and the source behind it - through JsTagger.tagVia. Astro locals become a bridge from middleware writes (they were a source unconditionally); Nitro event.context, the member and reactive bridges record provenance too. Precision: SvelteKit handler members are no longer tagged a second time (it doubled the app's flows to match labels); data functions keep framework-route and gain framework-data, whose return value goes to the page; React Router loaders count as resource routes only when exported from a module with no component; JSON responses neutralise; a server handler's new Response with an unfixed body is a sink unless served as data; a component's action/src props are no URL sinks (`<Form action={ack}>`); `this.href = v` is a component field; xhr.send, map.delete and cookies.delete are no SDK actions; useHead sinks are the head entry's values, not the call; NextResponse binds through the `next` package.
…e provenance
Single-file components (.vue, .svelte, .astro) cannot be resolved as functions; where an element
passes one a value a source reaches, the props entry (defineProps(), $props(), Astro.props) and a
Vue component's template reads of its declared props are tagged that source's kind with
provenance. Vue directives' binding parameters are sources only where a template binds the
directive to such a value (they were unconditionally). Both run in the bridge loop, after the
member and reactive bridges.
Provenance records the call-site arguments a value crossed (`store.setTerm(route.query.term)`),
not only the source and the bridged write. The source search follows a definition reached through
another read's definitions to what it was assigned - a template's `article` read behind
`v-if="article"` reached `article = _tmp.data` and stopped.
Vue <script setup> props declared by type or object (`defineProps<{ html: string }>()`) make the
template's bare reads of them sources.
…etServerSideProps, wrappers, registries
Bridges that join what a framework hands from one place to another by convention, each only
where a source reaches the value, with provenance: Svelte `use:` actions (conditional now, like
Vue directives), Alpine.data components property by property (a sanitising getter stays clean),
Vue Options API computed/data into the template, Next.js getServerSideProps/getStaticProps props
into the page (entrypoints found by export name too). Calls of SolidStart query/action wrappers
resolve to the wrapped function; calls of a function looked up in a Map resolve to what was put
into it, through a registering helper's parameter and namespace-import members.
Fixes found on the way:
- JsIndexes.assignedValues counted a nested function's assignments as the method's own, so a
callback that destructures the same names made the outer variable ambiguous and origin gave up
(forkJoin([a, b]).subscribe(([a, b]) => ...)).
- jssrc2cpg lowered `x satisfies T` without a reference to a function `x`, so `export const
getServerSideProps = (async () => ...) satisfies ...` assigned nothing.
- <operator>.interpolation and <operator>.jsxProps no longer generate definitions of their
arguments: a template's read of one field reached every later read off the same object (an
Alpine sanitising getter was reported). The operator set lives in an object - generation runs
while the transfer function is constructed.
- The identifier-base edge GLM added to the nested-container rule is gone: with every template
value interpolated, the walk no longer starts at a bare field read, and the edge only spread
one field's taint to its siblings.
- The provenance search names every source an object carries (bounded), so a page's props from
`{ props: { id, notice } }` name both.
- A URL prefix that is a query or fragment of the current page, or a relative path segment, keeps
the origin; hasFixedOrigin shares fixesOrigin.
This was referenced Oct 12, 2026
…ound SQL parameters, schema parses; no binding to implicit receivers
…ds, hook and composable returns by member, refs written later, captured parameters, imported directives, provenance keeps the chain
…ndow messages, template literals and implicit receivers define nothing
- JsPathAliases: tsconfig paths/baseUrl, a package's own name with exports/imports, and the @/ ~/ $lib conventions resolve to project files; JsModules and ImportResolverPass use them - Ember: route model() -> template @model, controller queryParams -> route template reads, getter reads, modifiers resolved to the wrapped callback; .gjs/.gts/.hbs components take their @Args through the prop links - BabelJsonParser slices code from the source astgen sends for a rewritten file - fetch: the response comes from the URL and the receiver, not the request body - new URL('<fixed path>', base) has a fixed origin; bind() arguments, hook returns, anchored zod regexes
…tack Start server functions and routes; SolidStart API routes; renamed and nested destructured parameters
- Ember: injected service calls and fields, helpers, controller getters in route templates, {{#each}} items, router.currentURL
- TanStack Start: createServerFn handlers take client data and resolve their callers (useServerFn too), a guarding validator sanitises data, server-route handlers, loaderDeps -> loader deps, redirect({ href })
- SolidStart API routes, @solidjs/router redirect, Solid signal getter reads
- jssrc2cpg: { key: local } and { key: { inner } } parameters bind the right locals
- getServerSideProps redirect destination is a sink; res.json and literal headers are no output; SVG responses are markup; constant Map lookups sanitise; this.member callbacks resolve
…o feat/js-frontend-frameworks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This teaches the taggers about frontend frameworks directly, so atom's reachables finds real flows in SPAs and their server routes without a cdxgen SBOM. It covers Angular, React (Next.js, React Router, TanStack, Solid, Qwik), Vue (Nuxt, Nitro), Svelte/SvelteKit, Astro, Lit, Alpine, Ember, jQuery and plain DOM code. Package purls for sinks come from the project's own package.json and lockfile.
The recognizers live in
taggers/js, one per framework, and match on import bindings, Angular DI types and file conventions rather than code regexes. Where a framework hands a value across by convention (a loader touseLoaderData, parent to child props, stores, directives, middleware locals), a bridge joins the two only when a source actually reaches the value, and tags the consumer withvia:<file>#<line>so you can see where it came from.Along the way: a few JavaScript dataflow fixes (nested member reads, closure captures, template operators that only read their arguments),
satisfieslowering, component elements lowered to calls of the component, and much less EasyTagsPass noise on JS. It covers what #196 set out to do for signal inputs.On a labelled corpus of 12 apps this finds 232 of 237 flows with no false positives (44 of 227 before). A hold-out set written blind scores 26 of 56 with 20 false positives, so there is more to do, mostly around props precision and allow-list style validation.
Needs to be released before AppThreat/atom#279. The astgen side is AppThreat/atom-parsetools#18.