Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver

Measured on 103 pinned projects (24 new open-source ones of kinds not tried before, among them intentionally vulnerable Rails, Node, GraphQL, C# and Java apps, a Deno framework, a WordPress plugin, a cookiecutter template and projects in Kotlin, Swift, Elixir and C, and 8 more of the maintainer's own), with findings labeled by hand: on the 70 labeled projects JevGate was tuned on, 75% of reviews were right against 69% with 0.20.0 (136 wrong reviews against 192), and 72% of considers against 65% (254 wrong considers against 354); on 11 held-out projects, 61% of reviews against 57%, and 56% of considers against 54%. Undecided units went from 2.2% to 1.5% of judged units.

- Server templates: ERB, EJS, JSP, Handlebars, Mustache, Nunjucks, Twig, Jinja and Go templates, and HTML under `templates/`, `views/`, `layouts/`, `partials/` or `includes/`, are judged. Their inline `<script>` code is parsed with the template's tags blanked and judged as the page's JavaScript in the visitor's browser, its top-level statements by every security rule, as a PHP page script is. Their `template code` is one more unit: each tag that writes request, cookie, session or signed-in-user data unescaped (ERB `raw` and `html_safe`, `<%== … %>`, EJS `<%- … %>`, `{{{ … }}}`, `|safe`, `|raw`), judged by injection, and every scriptlet and declaration of a JSP page once one reads the request, judged by every security rule. A template holding neither is not selected, so 900 of the corpus's 1,003 templates stay out of reports. On the intentionally vulnerable apps, 24 documented vulnerabilities that no rule read are found: RailsGoat's and DVNA's three XSS each, DVGA's paste-page XSS, DVJA's reflected XSS, and JavaVulnerableLab's JSP-only SQL, HQL and command injections, XSS, path traversal, SSRF and leaked stack traces. Of the labeled findings in templates, 42 of 49 reviews and 16 of 25 considers were right. About $0.1 on the corpus.
- Node views: a handler that renders a view by name (`res.render('app/products', …)`, a view under `views/` named without its extension) is sent the view's lines that write values unescaped (EJS `<%- … %>`, Handlebars `{{{ … }}}`, Pug `!=`, Nunjucks's and Swig's `|safe`), as a Django view is sent its templates, and its presence question, markup check and markup Choice name such templates. DVNA's reflected XSS in the product search is a review and the stored XSS of its product list a consider; only such handlers' requests change.
- A check no longer panics on text of several bytes: splitting SQL stepped into a character (pgweb's `booktown.sql` holds U+FFFD outside quotes), and locating a Python block that ends in a comment ending in `线` (vnpy) sliced inside it; both aborted the run with exit 101.
- An agent instruction file is looked up by its exact name: on a case-insensitive file system (macOS, Windows), probing for `AGENTS.md` also opened refined-github's `agents.md`, and `CLAUDE.md` a React Native template's `claude.md`, whose read then failed as a symlinked path and left the whole run incomplete (exit 2). An instruction file that links to a file no harness reads by name, such as refined-github's `CLAUDE.md` pointing at that `agents.md`, now loads it: the target is judged as that harness's instructions and counted once toward what a session loads.
- A document that is not text (NUL bytes, or not UTF-8) is skipped with a reason, as a source file is, instead of making the run incomplete: one Markdown file in dvja's docs failed the whole check with exit 2.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Consider (2):
| Security | Injection, sensitive data, unsafe settings, SQL access control, GitHub workflows; each finding names a CWE | `--rule security` |
| Documentation | Agent instruction files, large and stale docs, duplicated sections, code comments | `--rule documentation` |

It reads Rust, Python, JavaScript, TypeScript, Go, C#, Ruby, PHP and Java, the scripts of Astro, Vue and Svelte files, SQL for PostgreSQL and Supabase, GitHub Actions workflows, and Markdown, MDX, reStructuredText and AsciiDoc, and knows the routes, handlers and settings of frameworks from Express, Next.js and SvelteKit to Django, Laravel, ASP.NET Core and Spring MVC. [What it finds](https://tech-byte-frontier.github.io/jevgate/what-it-finds.html) and [supported languages and frameworks](https://tech-byte-frontier.github.io/jevgate/languages.html) have the details; `jevgate rules` prints every rule with the question it asks.
It reads Rust, Python, JavaScript, TypeScript, Go, C#, Ruby, PHP and Java, the scripts of Astro, Vue and Svelte files and the inline scripts of server templates (ERB, EJS, JSP, Handlebars, Jinja and others), SQL for PostgreSQL and Supabase, GitHub Actions workflows, and Markdown, MDX, reStructuredText and AsciiDoc, and knows the routes, handlers and settings of frameworks from Express, Next.js and SvelteKit to Django, Laravel, ASP.NET Core and Spring MVC. [What it finds](https://tech-byte-frontier.github.io/jevgate/what-it-finds.html) and [supported languages and frameworks](https://tech-byte-frontier.github.io/jevgate/languages.html) have the details; `jevgate rules` prints every rule with the question it asks.

## Install

Expand Down
24 changes: 22 additions & 2 deletions docs/classification-cascade.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,22 @@ signatures, or one candidate pair.
names no method, so such tests had no code under test.
Astro, Vue and
Svelte files are parsed as their scripts: Astro frontmatter and `<script>`
contents, with every other byte a space, so lines stay the file's.
contents, with every other byte a space, so lines stay the file's. A
server template (ERB, EJS, JSP, Handlebars, Mustache, Nunjucks, Twig,
Jinja or Go, or HTML under `templates/`, `views/`, `layouts/`,
`partials/` or `includes/`) is selected only when it holds an inline
script, and parsed as its scripts the same way, with its tags blanked:
`<%= … %>`, `<%- … %>` and `{{ … }}` read as a name of the same length,
other tags as spaces. Its top-level script statements are the page's
code and are judged like a function by every security rule, as a PHP
page script is, and its requests say that the code runs in the
visitor's browser. Its server code that reads the request, a cookie, the
session or the signed-in user is one more unit, `template code`: each
tag that writes such a value unescaped (ERB `raw` and `html_safe`, EJS
`<%- … %>`, `{{{ … }}}`, `|safe`, `|raw`), and every scriptlet of a JSP
page once one reads the request. RailsGoat's `raw cookies[:font]` and
JavaVulnerableLab's scriptlet queries were read by no rule. A template
holding neither is not selected.
A file whose parse holds syntax errors is not judged, unless they are few
and small (at most three regions, an eighth of the source in all), since
grammars miss some valid code: tree-sitter-typescript reads a call
Expand Down Expand Up @@ -270,7 +285,12 @@ signatures, or one candidate pair.
URL routes that reach it (a `\d+` parameter holds digits), the templates
it renders that write values unescaped, and the module constants it
names; a management command is marked as run by hand, since its options
came back as another party's. A settings module is one unit whose
came back as another party's. A Node handler that renders a view by name
(`res.render('app/products', …)`, a view under `views/` named without
its extension) is sent the view's lines that write values unescaped, in
its engine's syntax (EJS `<%- … %>`, Handlebars `{{{ … }}}`, Pug `!=`,
Nunjucks's and Swig's `|safe`), and its presence question, markup check
and markup Choice name such templates, as Django's do. A settings module is one unit whose
statements are its settings, with secret literals redacted to their
length (a dotted path such as a secret-key getter is not a secret). It is
sent with the lines that select it (`DJANGO_SETTINGS_MODULE` in a
Expand Down
24 changes: 22 additions & 2 deletions site/src/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,22 @@ signatures, or one candidate pair.
names no method, so such tests had no code under test.
Astro, Vue and
Svelte files are parsed as their scripts: Astro frontmatter and `<script>`
contents, with every other byte a space, so lines stay the file's.
contents, with every other byte a space, so lines stay the file's. A
server template (ERB, EJS, JSP, Handlebars, Mustache, Nunjucks, Twig,
Jinja or Go, or HTML under `templates/`, `views/`, `layouts/`,
`partials/` or `includes/`) is selected only when it holds an inline
script, and parsed as its scripts the same way, with its tags blanked:
`<%= … %>`, `<%- … %>` and `{{ … }}` read as a name of the same length,
other tags as spaces. Its top-level script statements are the page's
code and are judged like a function by every security rule, as a PHP
page script is, and its requests say that the code runs in the
visitor's browser. Its server code that reads the request, a cookie, the
session or the signed-in user is one more unit, `template code`: each
tag that writes such a value unescaped (ERB `raw` and `html_safe`, EJS
`<%- … %>`, `{{{ … }}}`, `|safe`, `|raw`), and every scriptlet of a JSP
page once one reads the request. RailsGoat's `raw cookies[:font]` and
JavaVulnerableLab's scriptlet queries were read by no rule. A template
holding neither is not selected.
A file whose parse holds syntax errors is not judged, unless they are few
and small (at most three regions, an eighth of the source in all), since
grammars miss some valid code: tree-sitter-typescript reads a call
Expand Down Expand Up @@ -301,7 +316,12 @@ signatures, or one candidate pair.
URL routes that reach it (a `\d+` parameter holds digits), the templates
it renders that write values unescaped, and the module constants it
names; a management command is marked as run by hand, since its options
came back as another party's. A settings module is one unit whose
came back as another party's. A Node handler that renders a view by name
(`res.render('app/products', …)`, a view under `views/` named without
its extension) is sent the view's lines that write values unescaped, in
its engine's syntax (EJS `<%- … %>`, Handlebars `{{{ … }}}`, Pug `!=`,
Nunjucks's and Swig's `|safe`), and its presence question, markup check
and markup Choice name such templates, as Django's do. A settings module is one unit whose
statements are its settings, with secret literals redacted to their
length (a dotted path such as a secret-key getter is not a secret). It is
sent with the lines that select it (`DJANGO_SETTINGS_MODULE` in a
Expand Down
3 changes: 2 additions & 1 deletion site/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
| PHP | `.php` `.phtml` | ✅ | ✅ PHPUnit `…TestCase` classes, Pest `test`/`it` | ✅ | ✅ comments |
| Java | `.java` | ✅ | ✅ JUnit 4 and 5, TestNG: `@Test`, `@ParameterizedTest`, `@Nested`, JUnit 3 `TestCase` | ✅ | ✅ comments |
| Astro, Vue, Svelte | `.astro` `.vue` `.svelte` | ✅ scripts only | ➖ | ✅ scripts only | ✅ script comments |
| Server templates: ERB, EJS, JSP, Handlebars, Mustache, Nunjucks, Twig, Jinja, Go | `.erb` `.ejs` `.jsp` `.hbs` `.mustache` `.njk` `.twig` `.jinja` `.j2` `.tmpl` `.gohtml`, and `.html` under `templates/`, `views/`, `layouts/`, `partials/` or `includes/` | ✅ inline scripts only | ➖ | ✅ inline scripts, as the page's code in the visitor's browser; and the code that reads the request, a cookie, the session or the signed-in user: tags that write it unescaped (`<%= raw … %>`, `.html_safe`, `<%== … %>`, `<%- … %>`, `{{{ … }}}`, `\|safe`, `\|raw`) and a JSP page's scriptlets | ✅ script comments |
| SQL (PostgreSQL, Supabase) | `.sql` | ➖ | ➖ | ✅ access control | ➖ |
| GitHub Actions | `.github/workflows/*.yml` | ➖ | ➖ | ✅ workflows | ➖ |
| Markdown, MDX | `.md` `.mdx` at the root, in `docs/` or `doc/`, READMEs and CONTRIBUTING files; agent instruction files; Claude Code skills, commands and subagents | ➖ | ➖ | ➖ | ✅ |
| reStructuredText, AsciiDoc | `.rst` `.adoc` `.asciidoc`, in the same places | ➖ | ➖ | ➖ | ✅ |

| Framework or platform | What JevGate understands |
|---|---|
| Hono, Express, Fastify, Koa | Route handlers written inline (`app.post('/pages', async (c) => …)`); error handlers (`app.onError`, `setErrorHandler`, four-parameter Express middleware) |
| Hono, Express, Fastify, Koa | Route handlers written inline (`app.post('/pages', async (c) => …)`); error handlers (`app.onError`, `setErrorHandler`, four-parameter Express middleware); views rendered by name (`res.render('app/products')`), with the lines that write values unescaped (EJS `<%- … %>`, Handlebars `{{{ … }}}`, Pug `!=`, Nunjucks and Swig `\|safe`) |
| NestJS | Exception filters (`@Catch`) |
| Next.js (App Router and Pages Router) | Route handlers (`app/**/route.ts`), Server Actions (`'use server'` files and functions), `pages/api` routes, middleware, client components, error boundaries and pages are named to Jev with who calls them and where they run, so a Server Action's arguments read as client input and a client component's requests as the user's own; `dangerouslySetInnerHTML`, redirects to client-chosen URLs, raw Prisma and Drizzle queries (`$queryRawUnsafe`, `sql.raw`) as opposed to their binding tagged templates, `NEXT_PUBLIC_` secrets, and `next.config` headers |
| SvelteKit | Server load functions and form actions (`+page.server.js`, `export const actions = {…}`), endpoints (`+server.js`) and server hooks are named to Jev with who calls them, so their request, form data, URL and cookies read as client input, and `cookies.set` is read with its secure defaults |
Expand Down
2 changes: 1 addition & 1 deletion site/src/what-it-finds.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

| Rule | Covers |
|---|---|
| Injection | Variables reaching SQL, shell commands, evaluated code, HTML, file paths, outbound URLs or redirect targets without binding, escaping or checks; data from another party given to a deserializer that can build any object (`pickle`, `yaml.load`, `Marshal.load`, `ObjectInputStream`, node-serialize; in C#, types named by input or chosen by the data being deserialized; in PHP, `unserialize`) or to an XML parser that resolves external entities; in PHP, uploaded file names |
| Injection | Variables reaching SQL, shell commands, evaluated code, HTML, file paths, outbound URLs or redirect targets without binding, escaping or checks; data from another party given to a deserializer that can build any object (`pickle`, `yaml.load`, `Marshal.load`, `ObjectInputStream`, node-serialize; in C#, types named by input or chosen by the data being deserialized; in PHP, `unserialize`) or to an XML parser that resolves external entities; in PHP, uploaded file names; in server templates, request, cookie, session or signed-in-user data written unescaped (`<%= raw cookies[:font] %>`), JSP scriptlets that query or run commands with request parameters, and inline scripts that write `location.hash` into the page |
| Sensitive data | Passwords, tokens or personal data written to logs; internal error details sent to clients, judged per error message and once per error handler (`app.onError`, `setErrorHandler`, Express error middleware, Flask and FastAPI handlers, Django error views and `process_exception` middleware, Django REST framework's `EXCEPTION_HANDLER`, NestJS filters, axum `IntoResponse` and actix-web `ResponseError` for error types, ASP.NET Core exception handlers, PHP `set_exception_handler`, Slim and Laravel handler classes); in Django code, also the server's environment or settings sent to clients (`request.META`) |
| Unsafe settings | Certificate checks turned off, passwords kept as plain text or hashed with a fast hash, non-cryptographic random secrets, permissive CORS, session cookies without `Secure`/`HttpOnly`, secrets in environment variables the build puts into browser code (`NEXT_PUBLIC_`, `VITE_`), HTML escaping turned off (`autoescape: false`), tokens accepted without checking their signature or expiry, and signing or encryption keys written in the code; in C#, also developer exception pages outside development and secrets derived from data others know; in Django code, also debug mode for the deployed site, `csrf_exempt` views and secret keys written in settings |
| Access control | SQL row-level policies that let every user reach other users' rows or trust `user_metadata`; SECURITY DEFINER functions without a fixed `search_path` or a caller check; grants that open writes to every user. SpacetimeDB modules (TypeScript and Rust, any kind of application): public tables of users' private data, views that return other users' rows, reducers that change rows their arguments choose or admin-only settings without checking the caller, and scheduled reducers clients can call in 1.x |
Expand Down
24 changes: 18 additions & 6 deletions src/analysis/django/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ pub const TEMPLATES: usize = 3;
/// The name a template under a `templates` directory is rendered by: the
/// path after the last `templates` part.
pub fn template_name(relative: &Path) -> Option<String> {
let parts: Vec<&str> = relative.iter().filter_map(|p| p.to_str()).collect();
let at = parts.iter().rposition(|p| *p == "templates")?;
let extension = relative.extension().and_then(|e| e.to_str())?;
(at + 1 < parts.len() && matches!(extension, "html" | "htm" | "txt" | "xml" | "jinja" | "j2"))
.then(|| parts[at + 1..].join("/"))
matches!(extension, "html" | "htm" | "txt" | "xml" | "jinja" | "j2")
.then(|| name_under(relative, "templates"))
.flatten()
}

/// The path after the last part named `directory`, joined with `/`: the
/// name a framework renders a template by.
pub fn name_under(relative: &Path, directory: &str) -> Option<String> {
let parts: Vec<&str> = relative.iter().filter_map(|p| p.to_str()).collect();
let at = parts.iter().rposition(|p| *p == directory)?;
(at + 1 < parts.len()).then(|| parts[at + 1..].join("/"))
}

/// The lines of a template that output values without escaping.
Expand All @@ -50,12 +57,17 @@ pub fn unescaped_lines(text: &str) -> Vec<String> {
}

/// The templates a function names in a string literal, such as
/// `render(request, 'blog/post.html', …)` or `template_name = "blog/post.html"`.
/// `render(request, 'blog/post.html', …)`, `template_name = "blog/post.html"`
/// or a Node view's `res.render('app/products')`, with or without its
/// extension.
pub fn rendered<'t>(source: &str, templates: &'t [Template]) -> Vec<&'t Template> {
templates
.iter()
.filter(|t| {
source.contains(&format!("'{}'", t.name)) || source.contains(&format!("\"{}\"", t.name))
['\'', '"'].iter().any(|quote| {
source.contains(&format!("{quote}{}{quote}", t.name))
|| source.contains(&format!("{quote}{}.", t.name))
})
})
.collect()
}
2 changes: 2 additions & 0 deletions src/analysis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ pub mod ruby;
pub mod sites;
pub mod sql;
mod summary;
pub mod template_code;
pub mod test_map;
pub mod units;
pub mod views;
pub mod workflow;

use tree_sitter::Node;
Expand Down
Loading
Loading