new capacity bits (#67)

This commit is contained in:
Eli Peter 2026-05-07 01:29:31 -04:00 committed by GitHub
parent afaffc0df6
commit 7d0e7320e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 10591 additions and 231 deletions

View file

@ -1,6 +1,6 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 1 }
{ "id_prefix": "taint-open-redirect", "min_count": 1 }
],
"forbidden_findings": [],
"noise_budget": {

View file

@ -0,0 +1,18 @@
// Safe: query value routed through the project-local `stripCRLF` helper
// before being written to the response header.
package main
import (
"net/http"
"strings"
)
func stripCRLF(raw string) string {
return strings.ReplaceAll(strings.ReplaceAll(raw, "\r", ""), "\n", "")
}
func handler(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
safe := stripCRLF(lang)
w.Header().Set("X-Lang", safe)
}

View file

@ -0,0 +1,12 @@
// Unsafe: net/http `ResponseWriter.Header().Set` receives a value built from
// `r.URL.Query().Get`. HEADER_INJECTION fires on the value argument.
package main
import (
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
lang := r.URL.Query().Get("lang")
w.Header().Set("X-Lang", lang)
}

View file

@ -0,0 +1,16 @@
// Safe: request parameter routed through the project-local `stripCRLF`
// helper before being written to the response header.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class SafeSetHeader {
public static String stripCRLF(String raw) {
return raw.replace("\r", "").replace("\n", "");
}
public void handle(HttpServletRequest req, HttpServletResponse res) {
String lang = req.getParameter("lang");
String safe = stripCRLF(lang);
res.setHeader("X-Lang", safe);
}
}

View file

@ -0,0 +1,11 @@
// Unsafe: HttpServletResponse.setHeader receives a value built from a
// request parameter. HEADER_INJECTION fires on the value argument.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UnsafeSetHeader {
public void handle(HttpServletRequest req, HttpServletResponse res) {
String lang = req.getParameter("lang");
res.setHeader("X-Lang", lang);
}
}

View file

@ -0,0 +1,14 @@
// Safe: req.query.lang routed through the project-local `stripCRLF` helper
// before being written to the response header.
function stripCRLF(raw) {
return raw.replace(/[\r\n]/g, '');
}
function handler(req, res) {
const lang = req.query.lang;
const safe = stripCRLF(lang);
res.setHeader('X-Lang', safe);
res.end();
}
module.exports = handler;

View file

@ -0,0 +1,14 @@
// Safe: req.query.lang routed through the project-local `stripCRLF` helper
// (a registered HEADER_INJECTION sanitizer) before the subscript-set, so
// taint-header-injection stays clean.
function stripCRLF(raw) {
return raw.replace(/[\r\n]/g, '');
}
function handler(req, res) {
const lang = req.query.lang;
res.headers["X-Forwarded-By"] = stripCRLF(lang);
res.end();
}
module.exports = handler;

View file

@ -0,0 +1,9 @@
// Unsafe: Express `res.setHeader` receives a value built from req.query.
// HEADER_INJECTION fires on the value argument.
function handler(req, res) {
const lang = req.query.lang;
res.setHeader('X-Lang', lang);
res.end();
}
module.exports = handler;

View file

@ -0,0 +1,11 @@
// Unsafe: tainted req.query value flows into the bare-subscript header set
// `res.headers["X-Forwarded-By"] = lang`. The LHS-subscript classification
// path matches `res.headers` as a HEADER_INJECTION sink so this form fires
// alongside the explicit `setHeader` / `res.set` method-call shapes.
function handler(req, res) {
const lang = req.query.lang;
res.headers["X-Forwarded-By"] = lang;
res.end();
}
module.exports = handler;

View file

@ -0,0 +1,10 @@
<?php
// Safe: $_GET['lang'] routed through the project-local `strip_crlf` helper
// before concatenation.
function strip_crlf($raw) {
return str_replace(["\r", "\n"], ["", ""], $raw);
}
$lang = $_GET['lang'];
$safe = strip_crlf($lang);
header("X-Lang: " . $safe);

View file

@ -0,0 +1,6 @@
<?php
// Unsafe: $_GET['lang'] concatenated into a `header()` line. The bare
// `header` matcher (exact-match sigil) fires on the call. Tainted input
// without `\r\n` stripping permits response splitting.
$lang = $_GET['lang'];
header("X-Lang: " . $lang);

View file

@ -0,0 +1,15 @@
# Safe: request arg routed through `strip_crlf` before being added to the
# response headers.
from flask import request, make_response
def strip_crlf(raw):
return raw.replace("\r", "").replace("\n", "")
def handler():
lang = request.args.get("lang")
safe = strip_crlf(lang)
resp = make_response("ok")
resp.headers.add("X-Lang", safe)
return resp

View file

@ -0,0 +1,15 @@
# Safe: request arg routed through `strip_crlf` (a registered
# HEADER_INJECTION sanitizer) before the subscript-set, so
# taint-header-injection stays clean.
from flask import request, make_response
def strip_crlf(raw):
return raw.replace("\r", "").replace("\n", "")
def handler():
lang = request.args.get("lang")
response = make_response("ok")
response.headers["X-Forwarded-By"] = strip_crlf(lang)
return response

View file

@ -0,0 +1,10 @@
# Unsafe: Flask response.headers.add receives a value built from request
# args. HEADER_INJECTION fires on the value argument.
from flask import request, make_response
def handler():
lang = request.args.get("lang")
resp = make_response("ok")
resp.headers.add("X-Lang", lang)
return resp

View file

@ -0,0 +1,13 @@
# Unsafe: tainted request value flows into the bare-subscript header set
# `response.headers["X-Forwarded-By"] = lang`. The LHS-subscript
# classification path matches `response.headers` / `resp.headers` as a
# HEADER_INJECTION sink so this form fires alongside the explicit
# `headers.add` / `set_cookie` method-call shapes.
from flask import request, make_response
def handler():
lang = request.args.get("lang")
response = make_response("ok")
response.headers["X-Forwarded-By"] = lang
return response

View file

@ -0,0 +1,7 @@
# Safe: tainted request value routed through `strip_crlf` (a registered
# HEADER_INJECTION sanitizer) before the subscript-set, so taint-header-injection
# stays clean.
def handle(params, response)
lang = params["lang"]
response.headers["X-Forwarded-By"] = strip_crlf(lang)
end

View file

@ -0,0 +1,9 @@
# Unsafe: tainted request value flows into the bare-subscript header set
# `response.headers["X-Forwarded-By"] = lang`. The LHS-subscript
# classification path matches `response.headers` as a HEADER_INJECTION
# sink so this form fires alongside the explicit `set_header` /
# `add_header` method-call shapes.
def handle(params, response)
lang = params["lang"]
response.headers["X-Forwarded-By"] = lang
end

View file

@ -0,0 +1,14 @@
// Safe: env value routed through the project-local `strip_crlf` helper
// before being written to the response header.
use std::env;
fn strip_crlf(raw: &str) -> String {
raw.replace('\r', "").replace('\n', "")
}
fn handler(response: &mut http::Response<()>) {
let lang = env::var("LANG").unwrap_or_default();
let safe = strip_crlf(&lang);
let value = http::HeaderValue::from_str(&safe).unwrap();
response.headers_mut().insert("X-Lang", value);
}

View file

@ -0,0 +1,9 @@
// Unsafe: tainted env value flows into `response.headers_mut().insert`.
// HEADER_INJECTION fires on the value argument.
use std::env;
fn handler(response: &mut http::Response<()>) {
let lang = env::var("LANG").unwrap_or_default();
let value = http::HeaderValue::from_str(&lang).unwrap();
response.headers_mut().insert("X-Lang", value);
}

View file

@ -0,0 +1,12 @@
// Safe: req.query.lang routed through `stripCRLF` before being written to
// the response header.
function stripCRLF(raw: string): string {
return raw.replace(/[\r\n]/g, '');
}
export function handler(req: any, res: any): void {
const lang: string = req.query.lang;
const safe: string = stripCRLF(lang);
res.setHeader('X-Lang', safe);
res.end();
}

View file

@ -0,0 +1,12 @@
// Safe: req.query.lang routed through the project-local `stripCRLF` helper
// (a registered HEADER_INJECTION sanitizer) before the subscript-set, so
// taint-header-injection stays clean.
function stripCRLF(raw: string): string {
return raw.replace(/[\r\n]/g, '');
}
export function handler(req: any, res: any): void {
const lang: string = req.query.lang;
res.headers["X-Forwarded-By"] = stripCRLF(lang);
res.end();
}

View file

@ -0,0 +1,7 @@
// Unsafe: Express `res.setHeader` receives a value built from req.query.
// HEADER_INJECTION fires on the value argument.
export function handler(req: any, res: any): void {
const lang: string = req.query.lang;
res.setHeader('X-Lang', lang);
res.end();
}

View file

@ -0,0 +1,9 @@
// Unsafe: tainted req.query value flows into the bare-subscript header set
// `res.headers["X-Forwarded-By"] = lang`. The LHS-subscript classification
// path matches `res.headers` as a HEADER_INJECTION sink so this form fires
// alongside the explicit `setHeader` / `res.set` method-call shapes.
export function handler(req: any, res: any): void {
const lang: string = req.query.lang;
res.headers["X-Forwarded-By"] = lang;
res.end();
}

View file

@ -1,6 +1,6 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 1 }
{ "id_prefix": "taint-open-redirect", "min_count": 1 }
],
"forbidden_findings": [],
"noise_budget": {

View file

@ -0,0 +1,12 @@
/* Baseline: filter is a string literal, no LDAP_INJECTION finding. */
#include <ldap.h>
int do_lookup(LDAP *ld) {
LDAPMessage *res = NULL;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
"(objectClass=person)",
NULL, 0, NULL, NULL, NULL, 0, &res);
}

View file

@ -0,0 +1,19 @@
/* Safe: project-local sanitize_ldap_filter (matches the developer-named
* `sanitize_*` Sanitizer rule) clears caps on the user value before it
* reaches ldap_search_ext_s. */
#include <ldap.h>
#include <stdlib.h>
extern char *sanitize_ldap_filter(const char *raw);
int do_lookup(LDAP *ld) {
char *user_filter = getenv("USER_FILTER");
char *safe = sanitize_ldap_filter(user_filter);
LDAPMessage *res = NULL;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
safe,
NULL, 0, NULL, NULL, NULL, 0, &res);
}

View file

@ -0,0 +1,15 @@
/* Unsafe: tainted env-string passed straight as the LDAP filter argument
* to ldap_search_ext_s. LDAP_INJECTION fires on the filter (arg 3). */
#include <ldap.h>
#include <stdlib.h>
int do_lookup(LDAP *ld) {
char *user_filter = getenv("USER_FILTER");
LDAPMessage *res = NULL;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
user_filter,
NULL, 0, NULL, NULL, NULL, 0, &res);
}

View file

@ -0,0 +1,12 @@
// Baseline: literal filter, no taint reaches the sink.
#include <ldap.h>
int do_lookup(LDAP* ld) {
LDAPMessage* res = nullptr;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
"(objectClass=person)",
nullptr, 0, nullptr, nullptr, nullptr, 0, &res);
}

View file

@ -0,0 +1,18 @@
// Safe: developer-named sanitize_* helper clears caps on the user value
// before it reaches ldap_search_ext_s.
#include <cstdlib>
#include <ldap.h>
extern const char* sanitize_ldap_filter(const char* raw);
int do_lookup(LDAP* ld) {
const char* user_filter = std::getenv("USER_FILTER");
const char* safe = sanitize_ldap_filter(user_filter);
LDAPMessage* res = nullptr;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
safe,
nullptr, 0, nullptr, nullptr, nullptr, 0, &res);
}

View file

@ -0,0 +1,15 @@
// Unsafe: tainted env value passed straight as the LDAP filter argument to
// ldap_search_ext_s. LDAP_INJECTION fires on the filter argument (position 3).
#include <cstdlib>
#include <ldap.h>
int do_lookup(LDAP* ld) {
const char* user_filter = std::getenv("USER_FILTER");
LDAPMessage* res = nullptr;
return ldap_search_ext_s(
ld,
"ou=people,dc=example,dc=com",
LDAP_SCOPE_SUBTREE,
user_filter,
nullptr, 0, nullptr, nullptr, nullptr, 0, &res);
}

View file

@ -0,0 +1,20 @@
// Baseline: filter is a literal string, no taint reaches NewSearchRequest.
package ldap_baseline
import (
"github.com/go-ldap/ldap/v3"
)
func Lookup() {
conn, _ := ldap.DialURL("ldap://example.com")
req := ldap.NewSearchRequest(
"ou=people,dc=example,dc=com",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
"(objectClass=person)",
[]string{"cn"},
nil,
)
conn.Search(req)
}

View file

@ -0,0 +1,27 @@
// Safe: ldap.EscapeFilter applies RFC 4515 escaping before the user value
// is interpolated into the filter. Sanitizer(LDAP_INJECTION) clears the cap.
package ldap_safe
import (
"fmt"
"net/http"
"github.com/go-ldap/ldap/v3"
)
func Lookup(w http.ResponseWriter, r *http.Request) {
conn, _ := ldap.DialURL("ldap://example.com")
user := r.FormValue("user")
safe := ldap.EscapeFilter(user)
filter := fmt.Sprintf("(uid=%s)", safe)
req := ldap.NewSearchRequest(
"ou=people,dc=example,dc=com",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
filter,
[]string{"cn"},
nil,
)
conn.Search(req)
}

View file

@ -0,0 +1,28 @@
// Unsafe: form value concatenated into an LDAP filter passed to
// ldap.NewSearchRequest, then executed via conn.Search. The construction
// call is tagged Cap::LDAP_INJECTION on the filter argument so the finding
// fires here regardless of the eventual conn.Search execution site.
package ldap_unsafe
import (
"fmt"
"net/http"
"github.com/go-ldap/ldap/v3"
)
func Lookup(w http.ResponseWriter, r *http.Request) {
conn, _ := ldap.DialURL("ldap://example.com")
user := r.FormValue("user")
filter := fmt.Sprintf("(uid=%s)", user)
req := ldap.NewSearchRequest(
"ou=people,dc=example,dc=com",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, 0, false,
filter,
[]string{"cn"},
nil,
)
conn.Search(req)
}

View file

@ -0,0 +1,14 @@
// Baseline: the filter is a compile-time constant; no taint reaches the sink
// and no LDAP_INJECTION finding fires. Guards the rule against firing on
// safe-by-construction call sites that simply happen to hit a search API.
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
public class BaselineConstantLdap {
private DirContext ctx;
public Object lookup() throws Exception {
String filter = "(objectClass=person)";
return ctx.search("ou=people,dc=example,dc=com", filter, new SearchControls());
}
}

View file

@ -0,0 +1,19 @@
// Safe: the user-supplied substring is run through Spring LDAP's
// LdapEncoder.filterEncode (RFC 4515 escape) before being assembled into the
// filter. The Sanitizer(LDAP_INJECTION) clears the cap and the sink does not
// fire.
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.servlet.http.HttpServletRequest;
import org.springframework.ldap.support.LdapEncoder;
public class SafeLdapSearch {
private DirContext ctx;
public Object lookup(HttpServletRequest req) throws Exception {
String user = req.getParameter("user");
String safe = LdapEncoder.filterEncode(user);
String filter = "(uid=" + safe + ")";
return ctx.search("ou=people,dc=example,dc=com", filter, new SearchControls());
}
}

View file

@ -0,0 +1,17 @@
// Unsafe: attacker-controlled username concatenated into an LDAP filter passed
// to DirContext.search. The receiver `ctx` carries TypeKind::LdapClient via
// the declared `DirContext` type so type-qualified resolution rewrites the
// callee to `LdapClient.search` and the LDAP_INJECTION sink fires.
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.servlet.http.HttpServletRequest;
public class UnsafeLdapSearch {
private DirContext ctx;
public Object lookup(HttpServletRequest req) throws Exception {
String user = req.getParameter("user");
String filter = "(uid=" + user + ")";
return ctx.search("ou=people,dc=example,dc=com", filter, new SearchControls());
}
}

View file

@ -0,0 +1,11 @@
// Baseline: filter is a literal constant; no taint reaches the search call.
const ldap = require('ldapjs');
const client = ldap.createClient({ url: 'ldap://example.com' });
function lookup(_req, res) {
const filter = '(objectClass=person)';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err) => { res.json({ ok: !err }); });
}
module.exports = lookup;

View file

@ -0,0 +1,16 @@
// Safe: ldap-escape's `filter` helper escapes the user-controlled substring
// before it lands in the filter expression. Mirrors the unsafe sibling's
// bound-variable shape so only the sanitiser introduction differs.
const ldap = require('ldapjs');
const ldapEscape = require('ldap-escape');
const client = ldap.createClient({ url: 'ldap://example.com' });
function lookup(req, res) {
const user = req.query.user;
const safe = ldapEscape(user);
const filter = '(uid=' + safe + ')';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err) => { res.json({ ok: !err }); });
}
module.exports = lookup;

View file

@ -0,0 +1,16 @@
// Unsafe: ldapjs `client.search` receives a filter assembled from req.query.
// Bound-variable idiom: the closure-captured `client` carries
// `TypeKind::LdapClient` (forwarded from the top-level body to the function
// body by `taint::inject_external_type_facts`), so type-qualified receiver
// resolution rewrites `client.search` → `LdapClient.search`.
const ldap = require('ldapjs');
const client = ldap.createClient({ url: 'ldap://example.com' });
function lookup(req, res) {
const user = req.query.user;
const filter = '(uid=' + user + ')';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err) => { res.json({ ok: !err }); });
}
module.exports = lookup;

View file

@ -0,0 +1,4 @@
<?php
// Baseline: filter is a literal string, no taint reaches the sink.
$ds = ldap_connect("ldap://example.com");
$result = ldap_search($ds, "ou=people,dc=example,dc=com", "(objectClass=person)");

View file

@ -0,0 +1,9 @@
<?php
// Safe: ldap_escape() with LDAP_ESCAPE_FILTER (or default) sanitises the user
// substring before it lands in the filter. Sanitizer(LDAP_INJECTION) clears
// the cap so the sink does not fire.
$ds = ldap_connect("ldap://example.com");
$user = $_GET['user'];
$safe = ldap_escape($user, "", LDAP_ESCAPE_FILTER);
$filter = "(uid=" . $safe . ")";
$result = ldap_search($ds, "ou=people,dc=example,dc=com", $filter);

View file

@ -0,0 +1,7 @@
<?php
// Unsafe: $_GET['user'] concatenated into an LDAP filter and passed straight
// to ldap_search. LDAP_INJECTION fires on the filter argument.
$ds = ldap_connect("ldap://example.com");
$user = $_GET['user'];
$filter = "(uid=" . $user . ")";
$result = ldap_search($ds, "ou=people,dc=example,dc=com", $filter);

View file

@ -0,0 +1,10 @@
# Baseline: filter is a compile-time constant. No taint reaches `search_s` so
# no LDAP_INJECTION finding fires.
import ldap
def lookup():
conn = ldap.initialize("ldap://example.com")
return conn.search_s(
"ou=people,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(objectClass=person)"
)

View file

@ -0,0 +1,14 @@
# Safe: user-supplied substring run through `escape_filter_chars` (RFC 4515)
# before being concatenated into the filter. The sanitizer clears the
# LDAP_INJECTION cap so the sink does not fire.
import ldap
from ldap.filter import escape_filter_chars
from flask import request
def lookup():
conn = ldap.initialize("ldap://example.com")
user = request.form["user"]
safe = escape_filter_chars(user)
flt = "(uid=" + safe + ")"
return conn.search_s("ou=people,dc=example,dc=com", ldap.SCOPE_SUBTREE, flt)

View file

@ -0,0 +1,13 @@
# Unsafe: tainted form data concatenated into an LDAP filter and passed to
# python-ldap's `search_s`. The bound receiver `conn` is typed as LdapClient
# via `ldap.initialize`, and the suffix matcher on `search_s` also catches the
# call directly.
import ldap
from flask import request
def lookup():
conn = ldap.initialize("ldap://example.com")
user = request.form["user"]
flt = "(uid=" + user + ")"
return conn.search_s("ou=people,dc=example,dc=com", ldap.SCOPE_SUBTREE, flt)

View file

@ -0,0 +1,9 @@
# Baseline: filter is a literal string, no taint reaches the search call.
require "net/ldap"
class UsersController
def lookup
ldap = Net::LDAP.new(host: "ldap.example.com")
ldap.search(base: "ou=people,dc=example,dc=com", filter: "(objectClass=person)")
end
end

View file

@ -0,0 +1,13 @@
# Safe: Net::LDAP::Filter.escape applies RFC 4515 escaping before the value
# is interpolated into the filter. Sanitizer(LDAP_INJECTION) clears the cap.
require "net/ldap"
class UsersController
def lookup(params)
ldap = Net::LDAP.new(host: "ldap.example.com")
user = params[:user]
safe = Net::LDAP::Filter.escape(user)
filter = "(uid=#{safe})"
ldap.search(base: "ou=people,dc=example,dc=com", filter: filter)
end
end

View file

@ -0,0 +1,14 @@
# Unsafe: tainted Rails param interpolated into the LDAP filter passed to
# Net::LDAP#search. The receiver is constructed via Net::LDAP.new and
# carries TypeKind::LdapClient; type-qualified resolution rewrites
# `ldap.search` → `LdapClient.search`, firing LDAP_INJECTION.
require "net/ldap"
class UsersController
def lookup(params)
ldap = Net::LDAP.new(host: "ldap.example.com")
user = params[:user]
filter = "(uid=#{user})"
ldap.search(base: "ou=people,dc=example,dc=com", filter: filter)
end
end

View file

@ -0,0 +1,9 @@
// Baseline: filter is a literal constant; no taint reaches the search call.
import * as ldap from 'ldapjs';
const client = ldap.createClient({ url: 'ldap://example.com' });
export function lookup(_req: any, res: any) {
const filter = '(objectClass=person)';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err: any) => { res.json({ ok: !err }); });
}

View file

@ -0,0 +1,14 @@
// Safe: ldap-escape's `filter` helper escapes the user-controlled substring
// before it lands in the filter expression. Mirrors the unsafe sibling's
// bound-variable shape so only the sanitiser introduction differs.
import * as ldap from 'ldapjs';
import ldapEscape from 'ldap-escape';
const client = ldap.createClient({ url: 'ldap://example.com' });
export function lookup(req: any, res: any) {
const user = req.query.user;
const safe = ldapEscape(user);
const filter = '(uid=' + safe + ')';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err: any) => { res.json({ ok: !err }); });
}

View file

@ -0,0 +1,14 @@
// Unsafe: ldapjs `client.search` receives a filter assembled from req.query.
// Bound-variable idiom: the closure-captured `client` carries
// `TypeKind::LdapClient` (forwarded from the top-level body to the function
// body by `taint::inject_external_type_facts`), so type-qualified receiver
// resolution rewrites `client.search` → `LdapClient.search`.
import * as ldap from 'ldapjs';
const client = ldap.createClient({ url: 'ldap://example.com' });
export function lookup(req: any, res: any) {
const user = req.query.user;
const filter = '(uid=' + user + ')';
client.search('ou=people,dc=example,dc=com', { filter: filter }, (err: any) => { res.json({ ok: !err }); });
}

View file

@ -0,0 +1,27 @@
package handler
import (
"net/http"
"net/url"
)
const allowedHost = "trusted.example.com"
// Safe: tainted query value parsed via `url.Parse` then host pinned against
// `allowedHost`. Multi-statement form — `parsed, err := url.Parse(target)`
// happens on a separate line from the `parsed.Host == allowedHost` check.
// Recognised by PredicateKind::HostAllowlistValidated which clears
// Cap::OPEN_REDIRECT on the validated branch.
func SafeHostAllowlist(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("next")
parsed, err := url.Parse(target)
if err != nil {
http.Redirect(w, r, "/", http.StatusFound)
return
}
if parsed.Host == allowedHost {
http.Redirect(w, r, parsed.String(), http.StatusFound)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}

View file

@ -0,0 +1,23 @@
package handler
import (
"net/http"
"strings"
)
// validateRedirectUrl is a project-local allowlist helper: it requires a
// leading `/` to limit redirects to same-origin paths. Registered as a
// Sanitizer(OPEN_REDIRECT) by `labels/go.rs`.
func validateRedirectUrl(raw string) string {
if strings.HasPrefix(raw, "/") {
return raw
}
return "/"
}
// Safe: query arg routed through validateRedirectUrl allowlist.
func Safe(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("next")
safe := validateRedirectUrl(target)
http.Redirect(w, r, safe, http.StatusFound)
}

View file

@ -0,0 +1,26 @@
package handler
import (
"net/http"
"strings"
)
// ensureRelativeUrl enforces a leading `/` and rejects scheme-prefixed or
// protocol-relative values (`//evil.example`). Registered as a
// Sanitizer(OPEN_REDIRECT) by `labels/go.rs`.
func ensureRelativeUrl(raw string) string {
if !strings.HasPrefix(raw, "/") {
return "/"
}
if strings.HasPrefix(raw, "//") {
return "/"
}
return raw
}
// Safe: query arg routed through ensureRelativeUrl (relative-only).
func SafeRelative(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("next")
safe := ensureRelativeUrl(target)
http.Redirect(w, r, safe, http.StatusFound)
}

View file

@ -0,0 +1,9 @@
package handler
import "net/http"
// Unsafe: query arg flows directly into http.Redirect (3rd arg URL).
func Unsafe(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("next")
http.Redirect(w, r, target, http.StatusFound)
}

View file

@ -0,0 +1,16 @@
// Phase 05 follow-up: inline relative-URL sanitiser. Developer didn't
// extract `ensureRelativeUrl` into a named helper; the leading-slash
// check is inline. RelativeUrlValidated predicate strips OPEN_REDIRECT
// on the true branch so the redirect call is not flagged.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SafeInlineRelative {
public void handle(HttpServletRequest req, HttpServletResponse res) throws IOException {
String target = req.getParameter("next");
if (target != null && target.startsWith("/")) {
res.sendRedirect(target);
}
}
}

View file

@ -0,0 +1,17 @@
// Safe: request param routed through `validateRedirectUrl` allowlist
// before being passed to sendRedirect.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SafeRedirect {
public static String validateRedirectUrl(String raw) {
return raw != null && raw.startsWith("/") ? raw : "/";
}
public void handle(HttpServletRequest req, HttpServletResponse res) throws IOException {
String target = req.getParameter("next");
String safe = validateRedirectUrl(target);
res.sendRedirect(safe);
}
}

View file

@ -0,0 +1,20 @@
// Safe: request param routed through `ensureRelativeUrl` which enforces a
// leading `/` and rejects scheme-prefixed values (relative-only path).
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SafeRelativeRedirect {
public static String ensureRelativeUrl(String raw) {
if (raw == null || !raw.startsWith("/") || raw.startsWith("//")) {
return "/";
}
return raw;
}
public void handle(HttpServletRequest req, HttpServletResponse res) throws IOException {
String target = req.getParameter("next");
String safe = ensureRelativeUrl(target);
res.sendRedirect(safe);
}
}

View file

@ -0,0 +1,11 @@
// Unsafe: HttpServletResponse.sendRedirect receives a request-supplied URL.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UnsafeRedirect {
public void handle(HttpServletRequest req, HttpServletResponse res) throws IOException {
String target = req.getParameter("next");
res.sendRedirect(target);
}
}

View file

@ -0,0 +1,16 @@
// Phase 05 follow-up: Spring MVC controller-return open-redirect.
// `return "redirect:" + url` is Spring's view-name convention; the
// returned String becomes a 302 to whatever follows the prefix, so a
// request-supplied URL flows straight into the redirect target.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class UnsafeSpringRedirect {
@RequestMapping("/login/post")
public String afterLogin(@RequestParam("next") String next, javax.servlet.http.HttpServletRequest req) {
String target = req.getParameter("next");
return "redirect:" + target;
}
}

View file

@ -0,0 +1,16 @@
// Safe: req.query.next routed through `new URL(...).host === ALLOWED`
// host-allowlist gate before reaching res.redirect. Recognised by
// PredicateKind::HostAllowlistValidated which clears Cap::OPEN_REDIRECT
// on the validated branch.
const ALLOWED_HOST = "trusted.example.com";
function handler(req, res) {
const target = req.query.next;
if (new URL(target).host === ALLOWED_HOST) {
res.redirect(target);
return;
}
res.redirect("/");
}
module.exports = handler;

View file

@ -0,0 +1,13 @@
// Safe: req.query.next routed through `validateRedirectUrl` allowlist
// before being passed to res.redirect.
function validateRedirectUrl(raw) {
return raw.startsWith('/') ? raw : '/';
}
function handler(req, res) {
const target = req.query.next;
const safe = validateRedirectUrl(target);
res.redirect(safe);
}
module.exports = handler;

View file

@ -0,0 +1,16 @@
// Safe: req.query.next routed through `ensureRelativeUrl` which enforces
// a leading `/` and rejects scheme-prefixed values (relative-only path).
function ensureRelativeUrl(raw) {
if (typeof raw !== 'string' || !raw.startsWith('/') || raw.startsWith('//')) {
return '/';
}
return raw;
}
function handler(req, res) {
const target = req.query.next;
const safe = ensureRelativeUrl(target);
res.redirect(safe);
}
module.exports = handler;

View file

@ -0,0 +1,7 @@
// Unsafe: req.query.next flows directly into res.redirect.
function handler(req, res) {
const target = req.query.next;
res.redirect(target);
}
module.exports = handler;

View file

@ -0,0 +1,13 @@
<?php
// Safe: $_GET['next'] is allowlisted via a developer-named
// `validateRedirectUrl` sanitizer (registered as
// `Sanitizer(OPEN_REDIRECT)` by the JS/TS rule and mirrored for PHP via
// the same matcher list — see `labels/php.rs` `validate_redirect_url` /
// `is_safe_redirect`) before being concatenated into the header line.
function validateRedirectUrl($raw) {
return strpos($raw, '/') === 0 ? $raw : '/';
}
$next = $_GET['next'];
$safe = validateRedirectUrl($next);
header("Location: " . $safe);

View file

@ -0,0 +1,13 @@
<?php
// Safe: $_GET['next'] routed through `ensure_relative_url` which enforces
// a leading `/` and rejects scheme-prefixed values (relative-only path).
function ensure_relative_url($raw) {
if (!is_string($raw) || strpos($raw, '/') !== 0 || strpos($raw, '//') === 0) {
return '/';
}
return $raw;
}
$next = $_GET['next'];
$safe = ensure_relative_url($next);
header("Location: " . $safe);

View file

@ -0,0 +1,7 @@
<?php
// Unsafe: $_GET['next'] flows directly into a `header("Location: ...")`
// line. The PHP gated SinkGate for `header` activates on the
// `Location:` first-arg prefix and emits OPEN_REDIRECT in addition to
// the existing flat HEADER_INJECTION sink.
$next = $_GET['next'];
header("Location: " . $next);

View file

@ -0,0 +1,15 @@
# Safe: query arg routed through `urlparse(...).netloc == ALLOWED`
# host-allowlist gate before passing to flask.redirect. Recognised by
# PredicateKind::HostAllowlistValidated which clears Cap::OPEN_REDIRECT
# on the validated branch.
from flask import request, redirect
from urllib.parse import urlparse
ALLOWED_HOST = "trusted.example.com"
def handler():
target = request.args.get("next")
if urlparse(target).netloc == ALLOWED_HOST:
return redirect(target)
return redirect("/")

View file

@ -0,0 +1,13 @@
# Safe: query arg routed through `validate_redirect_url` allowlist before
# passing to flask.redirect.
from flask import request, redirect
def validate_redirect_url(raw):
return raw if raw.startswith("/") else "/"
def handler():
target = request.args.get("next")
safe = validate_redirect_url(target)
return redirect(safe)

View file

@ -0,0 +1,15 @@
# Safe: query arg routed through `ensure_relative_url` which enforces a
# leading `/` and rejects scheme-prefixed values (relative-only path).
from flask import request, redirect
def ensure_relative_url(raw):
if not isinstance(raw, str) or not raw.startswith("/") or raw.startswith("//"):
return "/"
return raw
def handler():
target = request.args.get("next")
safe = ensure_relative_url(target)
return redirect(safe)

View file

@ -0,0 +1,8 @@
# Unsafe: Flask `redirect(url)` receives the user-controlled `next` query
# argument directly.
from flask import request, redirect
def handler():
target = request.args.get("next")
return redirect(target)

View file

@ -0,0 +1,13 @@
# Safe: query arg routed through `validate_redirect_url` allowlist before
# being passed to redirect_to.
class HomeController
def validate_redirect_url(raw)
raw.is_a?(String) && raw.start_with?('/') ? raw : '/'
end
def jump
target = params[:next]
safe = validate_redirect_url(target)
redirect_to safe
end
end

View file

@ -0,0 +1,16 @@
# Safe: query arg routed through `ensure_relative_url` which enforces a
# leading `/` and rejects scheme-prefixed values (relative-only path).
class HomeController
def ensure_relative_url(raw)
return '/' unless raw.is_a?(String)
return '/' unless raw.start_with?('/')
return '/' if raw.start_with?('//')
raw
end
def jump
target = params[:next]
safe = ensure_relative_url(target)
redirect_to safe
end
end

View file

@ -0,0 +1,7 @@
# Unsafe: Rails `redirect_to(url)` receives a request-supplied URL.
class HomeController
def jump
target = params[:next]
redirect_to target
end
end

View file

@ -0,0 +1,10 @@
// Safe: tainted value flows into a non-Location header. The actix
// builder gate only activates on `"Location"` so `"Content-Type"` headers
// stay clean.
use actix_web::HttpResponse;
use std::env;
fn render() -> HttpResponse {
let next = env::var("NEXT").unwrap_or_default();
HttpResponse::Ok().header("Content-Type", next).finish()
}

View file

@ -0,0 +1,19 @@
// Safe: tainted env value parsed via `url::Url::parse` then host pinned
// against `ALLOWED_HOST`. Multi-statement form — `parsed = Url::parse(x)`
// happens on a separate line from the `parsed.host_str() == Some(ALLOWED)`
// check. Recognised by PredicateKind::HostAllowlistValidated which clears
// Cap::OPEN_REDIRECT on the validated branch.
use axum::response::Redirect;
use std::env;
use url::Url;
const ALLOWED_HOST: &str = "trusted.example.com";
fn bounce() -> Redirect {
let next = env::var("NEXT").unwrap_or_default();
let parsed = Url::parse(&next).unwrap();
if parsed.host_str() == Some(ALLOWED_HOST) {
return Redirect::to(parsed.as_str());
}
Redirect::permanent("/")
}

View file

@ -0,0 +1,18 @@
// Safe: tainted value routed through `validate_redirect_url` allowlist
// before being passed to `Redirect::to`.
use axum::response::Redirect;
use std::env;
fn validate_redirect_url(raw: &str) -> String {
if raw.starts_with('/') {
raw.to_string()
} else {
"/".to_string()
}
}
fn bounce() -> Redirect {
let next = env::var("NEXT").unwrap_or_default();
let safe = validate_redirect_url(&next);
Redirect::to(&safe)
}

View file

@ -0,0 +1,18 @@
// Safe: tainted value routed through `ensure_relative_url` which enforces
// a leading `/` and rejects scheme-prefixed or protocol-relative values
// (relative-only path).
use axum::response::Redirect;
use std::env;
fn ensure_relative_url(raw: &str) -> String {
if !raw.starts_with('/') || raw.starts_with("//") {
return "/".to_string();
}
raw.to_string()
}
fn bounce() -> Redirect {
let next = env::var("NEXT").unwrap_or_default();
let safe = ensure_relative_url(&next);
Redirect::permanent(&safe)
}

View file

@ -0,0 +1,11 @@
// Unsafe: tainted env value flows directly into actix-web's
// `HttpResponse::Found().header("Location", url)` builder. Without an
// allowlist check, a tainted URL is the actix open-redirect vector.
use actix_web::HttpResponse;
use std::env;
fn bounce() -> HttpResponse {
let next = env::var("NEXT").unwrap_or_default();
let resp = HttpResponse::Found().header("Location", next);
resp.finish()
}

View file

@ -0,0 +1,13 @@
// Unsafe: tainted env value flows into actix-web's
// `HttpResponse::Found().header("Location", url)` builder, then chained
// `.finish()` returns the response in one expression. The chained
// `.finish()` is the outer call; without chained inner-gate rebinding
// the outer `.finish()` swallows classification and the inner `.header`
// open-redirect gate never fires.
use actix_web::HttpResponse;
use std::env;
fn bounce() -> HttpResponse {
let next = env::var("NEXT").unwrap_or_default();
HttpResponse::Found().header("Location", next).finish()
}

View file

@ -0,0 +1,9 @@
// Unsafe: tainted env value flows directly into `Redirect::to`, the axum
// open-redirect entry point.
use axum::response::Redirect;
use std::env;
fn bounce() -> Redirect {
let next = env::var("NEXT").unwrap_or_default();
Redirect::to(&next)
}

View file

@ -0,0 +1,14 @@
// Safe: req.query.next routed through `new URL(...).hostname === ALLOWED`
// host-allowlist gate before reaching res.redirect. Recognised by
// PredicateKind::HostAllowlistValidated which clears Cap::OPEN_REDIRECT
// on the validated branch.
const ALLOWED_HOST: string = "trusted.example.com";
export function handler(req: any, res: any): void {
const target: string = req.query.next;
if (new URL(target).hostname === ALLOWED_HOST) {
res.redirect(target);
return;
}
res.redirect("/");
}

View file

@ -0,0 +1,10 @@
// Safe: req.query.next routed through `validateRedirectUrl` allowlist.
function validateRedirectUrl(raw: string): string {
return raw.startsWith('/') ? raw : '/';
}
export function handler(req: any, res: any): void {
const target: string = req.query.next;
const safe: string = validateRedirectUrl(target);
res.redirect(safe);
}

View file

@ -0,0 +1,14 @@
// Safe: req.query.next routed through `ensureRelativeUrl` which enforces
// a leading `/` and rejects scheme-prefixed values (relative-only path).
function ensureRelativeUrl(raw: string): string {
if (!raw.startsWith('/') || raw.startsWith('//')) {
return '/';
}
return raw;
}
export function handler(req: any, res: any): void {
const target: string = req.query.next;
const safe: string = ensureRelativeUrl(target);
res.redirect(safe);
}

View file

@ -0,0 +1,5 @@
// Unsafe: req.query.next flows directly into res.redirect.
export function handler(req: any, res: any): void {
const target: string = req.query.next;
res.redirect(target);
}

View file

@ -0,0 +1,13 @@
// Phase 09: allowlist guard restricts the key to a known-safe constant
// set on the true arm of the `if`, so the enclosed assignment cannot
// reach `__proto__` / `constructor` even though `userKey` is tainted.
function handler(req, res) {
const target = {};
const userKey = req.query.k;
if (userKey === "name" || userKey === "id") {
target[userKey] = req.query.v;
}
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,11 @@
// Phase 09: `Object.create(null)` produces a null-prototype receiver
// that has no `Object.prototype` to mutate, so writes through any key
// (including `__proto__`) cannot pollute the global prototype chain.
function handler(req, res) {
const target = Object.create(null);
const userKey = req.query.k;
target[userKey] = req.query.v;
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,15 @@
// Phase 09: reject-list guard suppresses prototype pollution. The
// dangerous-key path terminates with `return`, so the assignment that
// follows only runs when `userKey` is provably not `__proto__` /
// `constructor` / `prototype`.
function handler(req, res) {
const target = {};
const userKey = req.query.k;
if (userKey === "__proto__" || userKey === "constructor" || userKey === "prototype") {
return;
}
target[userKey] = req.query.v;
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,11 @@
// Phase 09: tainted *key* in `obj[key] = val` is the prototype-pollution
// channel. When `req.query.k` resolves to `__proto__` / `constructor`, the
// assignment mutates `Object.prototype` globally.
function handler(req, res) {
const target = {};
const userKey = req.query.k;
target[userKey] = req.query.v;
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,19 @@
// Phase 09 flow-sensitive null-prototype guard. `target` is only
// initialised with `Object.create(null)` on one branch; the else branch
// leaves it as a plain object whose prototype chain is mutable. The
// prior AST-scan suppressor matched any same-function `Object.create(null)`
// assignment and silenced both branches; the SSA TypeFacts path joins
// to Unknown at the phi and keeps PROTOTYPE_POLLUTION on the unsafe path.
function handler(req, res) {
let target;
if (req.query.safe) {
target = Object.create(null);
} else {
target = {};
}
const userKey = req.query.k;
target[userKey] = req.query.v;
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,14 @@
// Safe: Backbone-style class extension shares the `extend` suffix but
// passes an object literal as arg 0, never the literal `true` deep flag.
// The bare `extend` SinkGate uses `LiteralOnly` activation so this call
// does not produce a PROTOTYPE_POLLUTION finding.
const Backbone = require('backbone');
const UserModel = Backbone.Model.extend({
defaults: { name: '', id: 0 },
initialize: function () {
this.set('createdAt', Date.now());
},
});
module.exports = UserModel;

View file

@ -0,0 +1,14 @@
// Safe: bare `extend` invoked with a dynamic flag value at arg 0. Without
// literal evidence that the deep-merge form is in use, the `LiteralOnly`
// gate suppresses (no conservative ALL_ARGS_PAYLOAD fire). This avoids
// over-firing on shallow `extend(target, src)` shapes (Underscore-style)
// where arg 0 is the target object, not a deep flag.
const { extend } = require('some-utility');
function handler(req, res) {
const target = {};
extend(target, req.body);
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,11 @@
// Safe: lodash `_.merge` invoked with a constant-source object. No taint
// reaches the merge so PROTOTYPE_POLLUTION does not fire.
const _ = require('lodash');
function build() {
const target = {};
_.merge(target, { a: 1, b: 2 });
return target;
}
module.exports = build;

View file

@ -0,0 +1,9 @@
// Safe: Object.assign with a constant-source object literal. No taint
// reaches the merge so PROTOTYPE_POLLUTION does not fire.
function build() {
const target = {};
Object.assign(target, { x: 1, y: 2 });
return target;
}
module.exports = build;

View file

@ -0,0 +1,11 @@
// Safe: `set-value` invoked with constant key + literal value. No tainted
// flow into the path or value position, no PROTOTYPE_POLLUTION.
const setValue = require('set-value');
function handler(req, res) {
const target = {};
setValue(target, "name", "alice");
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,14 @@
// Unsafe: jQuery's deep-merge `extend` imported as a bound name. Bare
// `extend(true, target, src)` with attacker-controlled `req.body` as a
// source argument can rewrite `Object.prototype` via `__proto__` keys in
// the merged input. PROTOTYPE_POLLUTION fires via the `LiteralOnly` gate
// keyed on the literal `true` deep-flag at arg 0.
const { extend } = require('jquery');
function handler(req, res) {
const target = {};
extend(true, target, req.body);
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,12 @@
// Unsafe: `dot-prop` standalone helper (CVE-2020-8116) invoked with an
// attacker-controlled `path`. Tainted path `__proto__.polluted` walks
// the prototype chain because dot-prop did not block prototype keys.
const dotProp = require('dot-prop');
function handler(req, res) {
const target = {};
dotProp.set(target, req.body.path, req.body.value);
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,12 @@
// Unsafe: jsonpath `jp.set(obj, path, value)` invoked with an
// attacker-controlled `path`. Tainted path with `__proto__` segments
// pollutes the prototype chain.
const jp = require('jsonpath');
function handler(req, res) {
const target = {};
jp.set(target, req.body.path, req.body.value);
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,12 @@
// Unsafe: lodash `_.merge` invoked with attacker-controlled `req.body` as
// the source argument. Tainted `__proto__` / `constructor` keys can rewrite
// Object.prototype globally. PROTOTYPE_POLLUTION fires.
const _ = require('lodash');
function handler(req, res) {
const target = {};
_.merge(target, req.body);
res.json(target);
}
module.exports = handler;

View file

@ -0,0 +1,8 @@
// Unsafe: Object.assign with attacker-controlled `req.body` source.
function handler(req, res) {
const target = {};
Object.assign(target, req.body);
res.json(target);
}
module.exports = handler;

Some files were not shown because too many files have changed in this diff Show more