mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
new capacity bits (#67)
This commit is contained in:
parent
afaffc0df6
commit
7d0e7320e2
261 changed files with 10591 additions and 231 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
18
tests/fixtures/header_injection/go/safe_set_header.go
vendored
Normal file
18
tests/fixtures/header_injection/go/safe_set_header.go
vendored
Normal 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)
|
||||
}
|
||||
12
tests/fixtures/header_injection/go/unsafe_set_header.go
vendored
Normal file
12
tests/fixtures/header_injection/go/unsafe_set_header.go
vendored
Normal 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)
|
||||
}
|
||||
16
tests/fixtures/header_injection/java/SafeSetHeader.java
vendored
Normal file
16
tests/fixtures/header_injection/java/SafeSetHeader.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
11
tests/fixtures/header_injection/java/UnsafeSetHeader.java
vendored
Normal file
11
tests/fixtures/header_injection/java/UnsafeSetHeader.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
14
tests/fixtures/header_injection/javascript/safe_set_header.js
vendored
Normal file
14
tests/fixtures/header_injection/javascript/safe_set_header.js
vendored
Normal 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;
|
||||
14
tests/fixtures/header_injection/javascript/safe_subscript_set.js
vendored
Normal file
14
tests/fixtures/header_injection/javascript/safe_subscript_set.js
vendored
Normal 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;
|
||||
9
tests/fixtures/header_injection/javascript/unsafe_set_header.js
vendored
Normal file
9
tests/fixtures/header_injection/javascript/unsafe_set_header.js
vendored
Normal 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;
|
||||
11
tests/fixtures/header_injection/javascript/unsafe_subscript_set.js
vendored
Normal file
11
tests/fixtures/header_injection/javascript/unsafe_subscript_set.js
vendored
Normal 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;
|
||||
10
tests/fixtures/header_injection/php/safe_set_header.php
vendored
Normal file
10
tests/fixtures/header_injection/php/safe_set_header.php
vendored
Normal 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);
|
||||
6
tests/fixtures/header_injection/php/unsafe_set_header.php
vendored
Normal file
6
tests/fixtures/header_injection/php/unsafe_set_header.php
vendored
Normal 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);
|
||||
15
tests/fixtures/header_injection/python/safe_set_header.py
vendored
Normal file
15
tests/fixtures/header_injection/python/safe_set_header.py
vendored
Normal 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
|
||||
15
tests/fixtures/header_injection/python/safe_subscript_set.py
vendored
Normal file
15
tests/fixtures/header_injection/python/safe_subscript_set.py
vendored
Normal 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
|
||||
10
tests/fixtures/header_injection/python/unsafe_set_header.py
vendored
Normal file
10
tests/fixtures/header_injection/python/unsafe_set_header.py
vendored
Normal 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
|
||||
13
tests/fixtures/header_injection/python/unsafe_subscript_set.py
vendored
Normal file
13
tests/fixtures/header_injection/python/unsafe_subscript_set.py
vendored
Normal 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
|
||||
7
tests/fixtures/header_injection/ruby/safe_subscript_set.rb
vendored
Normal file
7
tests/fixtures/header_injection/ruby/safe_subscript_set.rb
vendored
Normal 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
|
||||
9
tests/fixtures/header_injection/ruby/unsafe_subscript_set.rb
vendored
Normal file
9
tests/fixtures/header_injection/ruby/unsafe_subscript_set.rb
vendored
Normal 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
|
||||
14
tests/fixtures/header_injection/rust/safe_set_header.rs
vendored
Normal file
14
tests/fixtures/header_injection/rust/safe_set_header.rs
vendored
Normal 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);
|
||||
}
|
||||
9
tests/fixtures/header_injection/rust/unsafe_set_header.rs
vendored
Normal file
9
tests/fixtures/header_injection/rust/unsafe_set_header.rs
vendored
Normal 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);
|
||||
}
|
||||
12
tests/fixtures/header_injection/typescript/safe_set_header.ts
vendored
Normal file
12
tests/fixtures/header_injection/typescript/safe_set_header.ts
vendored
Normal 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();
|
||||
}
|
||||
12
tests/fixtures/header_injection/typescript/safe_subscript_set.ts
vendored
Normal file
12
tests/fixtures/header_injection/typescript/safe_subscript_set.ts
vendored
Normal 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();
|
||||
}
|
||||
7
tests/fixtures/header_injection/typescript/unsafe_set_header.ts
vendored
Normal file
7
tests/fixtures/header_injection/typescript/unsafe_set_header.ts
vendored
Normal 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();
|
||||
}
|
||||
9
tests/fixtures/header_injection/typescript/unsafe_subscript_set.ts
vendored
Normal file
9
tests/fixtures/header_injection/typescript/unsafe_subscript_set.ts
vendored
Normal 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();
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
12
tests/fixtures/ldap_injection/c/baseline_constant_ldap.c
vendored
Normal file
12
tests/fixtures/ldap_injection/c/baseline_constant_ldap.c
vendored
Normal 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);
|
||||
}
|
||||
19
tests/fixtures/ldap_injection/c/safe_ldap_search.c
vendored
Normal file
19
tests/fixtures/ldap_injection/c/safe_ldap_search.c
vendored
Normal 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);
|
||||
}
|
||||
15
tests/fixtures/ldap_injection/c/unsafe_ldap_search.c
vendored
Normal file
15
tests/fixtures/ldap_injection/c/unsafe_ldap_search.c
vendored
Normal 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);
|
||||
}
|
||||
12
tests/fixtures/ldap_injection/cpp/baseline_constant_ldap.cpp
vendored
Normal file
12
tests/fixtures/ldap_injection/cpp/baseline_constant_ldap.cpp
vendored
Normal 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);
|
||||
}
|
||||
18
tests/fixtures/ldap_injection/cpp/safe_ldap_search.cpp
vendored
Normal file
18
tests/fixtures/ldap_injection/cpp/safe_ldap_search.cpp
vendored
Normal 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);
|
||||
}
|
||||
15
tests/fixtures/ldap_injection/cpp/unsafe_ldap_search.cpp
vendored
Normal file
15
tests/fixtures/ldap_injection/cpp/unsafe_ldap_search.cpp
vendored
Normal 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);
|
||||
}
|
||||
20
tests/fixtures/ldap_injection/go/baseline_constant_ldap.go
vendored
Normal file
20
tests/fixtures/ldap_injection/go/baseline_constant_ldap.go
vendored
Normal 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)
|
||||
}
|
||||
27
tests/fixtures/ldap_injection/go/safe_ldap_search.go
vendored
Normal file
27
tests/fixtures/ldap_injection/go/safe_ldap_search.go
vendored
Normal 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)
|
||||
}
|
||||
28
tests/fixtures/ldap_injection/go/unsafe_ldap_search.go
vendored
Normal file
28
tests/fixtures/ldap_injection/go/unsafe_ldap_search.go
vendored
Normal 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)
|
||||
}
|
||||
14
tests/fixtures/ldap_injection/java/BaselineConstantLdap.java
vendored
Normal file
14
tests/fixtures/ldap_injection/java/BaselineConstantLdap.java
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
19
tests/fixtures/ldap_injection/java/SafeLdapSearch.java
vendored
Normal file
19
tests/fixtures/ldap_injection/java/SafeLdapSearch.java
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
17
tests/fixtures/ldap_injection/java/UnsafeLdapSearch.java
vendored
Normal file
17
tests/fixtures/ldap_injection/java/UnsafeLdapSearch.java
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
11
tests/fixtures/ldap_injection/javascript/baseline_constant_ldap.js
vendored
Normal file
11
tests/fixtures/ldap_injection/javascript/baseline_constant_ldap.js
vendored
Normal 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;
|
||||
16
tests/fixtures/ldap_injection/javascript/safe_ldap_search.js
vendored
Normal file
16
tests/fixtures/ldap_injection/javascript/safe_ldap_search.js
vendored
Normal 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;
|
||||
16
tests/fixtures/ldap_injection/javascript/unsafe_ldap_search.js
vendored
Normal file
16
tests/fixtures/ldap_injection/javascript/unsafe_ldap_search.js
vendored
Normal 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;
|
||||
4
tests/fixtures/ldap_injection/php/baseline_constant_ldap.php
vendored
Normal file
4
tests/fixtures/ldap_injection/php/baseline_constant_ldap.php
vendored
Normal 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)");
|
||||
9
tests/fixtures/ldap_injection/php/safe_ldap_search.php
vendored
Normal file
9
tests/fixtures/ldap_injection/php/safe_ldap_search.php
vendored
Normal 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);
|
||||
7
tests/fixtures/ldap_injection/php/unsafe_ldap_search.php
vendored
Normal file
7
tests/fixtures/ldap_injection/php/unsafe_ldap_search.php
vendored
Normal 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);
|
||||
10
tests/fixtures/ldap_injection/python/baseline_constant_ldap.py
vendored
Normal file
10
tests/fixtures/ldap_injection/python/baseline_constant_ldap.py
vendored
Normal 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)"
|
||||
)
|
||||
14
tests/fixtures/ldap_injection/python/safe_ldap_search.py
vendored
Normal file
14
tests/fixtures/ldap_injection/python/safe_ldap_search.py
vendored
Normal 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)
|
||||
13
tests/fixtures/ldap_injection/python/unsafe_ldap_search.py
vendored
Normal file
13
tests/fixtures/ldap_injection/python/unsafe_ldap_search.py
vendored
Normal 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)
|
||||
9
tests/fixtures/ldap_injection/ruby/baseline_constant_ldap.rb
vendored
Normal file
9
tests/fixtures/ldap_injection/ruby/baseline_constant_ldap.rb
vendored
Normal 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
|
||||
13
tests/fixtures/ldap_injection/ruby/safe_ldap_search.rb
vendored
Normal file
13
tests/fixtures/ldap_injection/ruby/safe_ldap_search.rb
vendored
Normal 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
|
||||
14
tests/fixtures/ldap_injection/ruby/unsafe_ldap_search.rb
vendored
Normal file
14
tests/fixtures/ldap_injection/ruby/unsafe_ldap_search.rb
vendored
Normal 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
|
||||
9
tests/fixtures/ldap_injection/typescript/baseline_constant_ldap.ts
vendored
Normal file
9
tests/fixtures/ldap_injection/typescript/baseline_constant_ldap.ts
vendored
Normal 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 }); });
|
||||
}
|
||||
14
tests/fixtures/ldap_injection/typescript/safe_ldap_search.ts
vendored
Normal file
14
tests/fixtures/ldap_injection/typescript/safe_ldap_search.ts
vendored
Normal 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 }); });
|
||||
}
|
||||
14
tests/fixtures/ldap_injection/typescript/unsafe_ldap_search.ts
vendored
Normal file
14
tests/fixtures/ldap_injection/typescript/unsafe_ldap_search.ts
vendored
Normal 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 }); });
|
||||
}
|
||||
27
tests/fixtures/open_redirect/go/safe_host_allowlist_redirect.go
vendored
Normal file
27
tests/fixtures/open_redirect/go/safe_host_allowlist_redirect.go
vendored
Normal 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)
|
||||
}
|
||||
23
tests/fixtures/open_redirect/go/safe_redirect.go
vendored
Normal file
23
tests/fixtures/open_redirect/go/safe_redirect.go
vendored
Normal 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)
|
||||
}
|
||||
26
tests/fixtures/open_redirect/go/safe_relative_redirect.go
vendored
Normal file
26
tests/fixtures/open_redirect/go/safe_relative_redirect.go
vendored
Normal 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)
|
||||
}
|
||||
9
tests/fixtures/open_redirect/go/unsafe_redirect.go
vendored
Normal file
9
tests/fixtures/open_redirect/go/unsafe_redirect.go
vendored
Normal 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)
|
||||
}
|
||||
16
tests/fixtures/open_redirect/java/SafeInlineRelative.java
vendored
Normal file
16
tests/fixtures/open_redirect/java/SafeInlineRelative.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
tests/fixtures/open_redirect/java/SafeRedirect.java
vendored
Normal file
17
tests/fixtures/open_redirect/java/SafeRedirect.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
20
tests/fixtures/open_redirect/java/SafeRelativeRedirect.java
vendored
Normal file
20
tests/fixtures/open_redirect/java/SafeRelativeRedirect.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
11
tests/fixtures/open_redirect/java/UnsafeRedirect.java
vendored
Normal file
11
tests/fixtures/open_redirect/java/UnsafeRedirect.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
16
tests/fixtures/open_redirect/java/UnsafeSpringRedirect.java
vendored
Normal file
16
tests/fixtures/open_redirect/java/UnsafeSpringRedirect.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
16
tests/fixtures/open_redirect/javascript/safe_host_allowlist_redirect.js
vendored
Normal file
16
tests/fixtures/open_redirect/javascript/safe_host_allowlist_redirect.js
vendored
Normal 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;
|
||||
13
tests/fixtures/open_redirect/javascript/safe_redirect.js
vendored
Normal file
13
tests/fixtures/open_redirect/javascript/safe_redirect.js
vendored
Normal 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;
|
||||
16
tests/fixtures/open_redirect/javascript/safe_relative_redirect.js
vendored
Normal file
16
tests/fixtures/open_redirect/javascript/safe_relative_redirect.js
vendored
Normal 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;
|
||||
7
tests/fixtures/open_redirect/javascript/unsafe_redirect.js
vendored
Normal file
7
tests/fixtures/open_redirect/javascript/unsafe_redirect.js
vendored
Normal 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;
|
||||
13
tests/fixtures/open_redirect/php/safe_redirect.php
vendored
Normal file
13
tests/fixtures/open_redirect/php/safe_redirect.php
vendored
Normal 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);
|
||||
13
tests/fixtures/open_redirect/php/safe_relative_redirect.php
vendored
Normal file
13
tests/fixtures/open_redirect/php/safe_relative_redirect.php
vendored
Normal 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);
|
||||
7
tests/fixtures/open_redirect/php/unsafe_redirect.php
vendored
Normal file
7
tests/fixtures/open_redirect/php/unsafe_redirect.php
vendored
Normal 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);
|
||||
15
tests/fixtures/open_redirect/python/safe_host_allowlist_redirect.py
vendored
Normal file
15
tests/fixtures/open_redirect/python/safe_host_allowlist_redirect.py
vendored
Normal 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("/")
|
||||
13
tests/fixtures/open_redirect/python/safe_redirect.py
vendored
Normal file
13
tests/fixtures/open_redirect/python/safe_redirect.py
vendored
Normal 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)
|
||||
15
tests/fixtures/open_redirect/python/safe_relative_redirect.py
vendored
Normal file
15
tests/fixtures/open_redirect/python/safe_relative_redirect.py
vendored
Normal 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)
|
||||
8
tests/fixtures/open_redirect/python/unsafe_redirect.py
vendored
Normal file
8
tests/fixtures/open_redirect/python/unsafe_redirect.py
vendored
Normal 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)
|
||||
13
tests/fixtures/open_redirect/ruby/safe_redirect.rb
vendored
Normal file
13
tests/fixtures/open_redirect/ruby/safe_redirect.rb
vendored
Normal 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
|
||||
16
tests/fixtures/open_redirect/ruby/safe_relative_redirect.rb
vendored
Normal file
16
tests/fixtures/open_redirect/ruby/safe_relative_redirect.rb
vendored
Normal 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
|
||||
7
tests/fixtures/open_redirect/ruby/unsafe_redirect.rb
vendored
Normal file
7
tests/fixtures/open_redirect/ruby/unsafe_redirect.rb
vendored
Normal 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
|
||||
10
tests/fixtures/open_redirect/rust/safe_actix_content_type.rs
vendored
Normal file
10
tests/fixtures/open_redirect/rust/safe_actix_content_type.rs
vendored
Normal 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()
|
||||
}
|
||||
19
tests/fixtures/open_redirect/rust/safe_host_allowlist_redirect.rs
vendored
Normal file
19
tests/fixtures/open_redirect/rust/safe_host_allowlist_redirect.rs
vendored
Normal 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("/")
|
||||
}
|
||||
18
tests/fixtures/open_redirect/rust/safe_redirect.rs
vendored
Normal file
18
tests/fixtures/open_redirect/rust/safe_redirect.rs
vendored
Normal 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)
|
||||
}
|
||||
18
tests/fixtures/open_redirect/rust/safe_relative_redirect.rs
vendored
Normal file
18
tests/fixtures/open_redirect/rust/safe_relative_redirect.rs
vendored
Normal 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)
|
||||
}
|
||||
11
tests/fixtures/open_redirect/rust/unsafe_actix_location.rs
vendored
Normal file
11
tests/fixtures/open_redirect/rust/unsafe_actix_location.rs
vendored
Normal 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()
|
||||
}
|
||||
13
tests/fixtures/open_redirect/rust/unsafe_actix_location_chained.rs
vendored
Normal file
13
tests/fixtures/open_redirect/rust/unsafe_actix_location_chained.rs
vendored
Normal 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()
|
||||
}
|
||||
9
tests/fixtures/open_redirect/rust/unsafe_redirect.rs
vendored
Normal file
9
tests/fixtures/open_redirect/rust/unsafe_redirect.rs
vendored
Normal 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)
|
||||
}
|
||||
14
tests/fixtures/open_redirect/typescript/safe_host_allowlist_redirect.ts
vendored
Normal file
14
tests/fixtures/open_redirect/typescript/safe_host_allowlist_redirect.ts
vendored
Normal 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("/");
|
||||
}
|
||||
10
tests/fixtures/open_redirect/typescript/safe_redirect.ts
vendored
Normal file
10
tests/fixtures/open_redirect/typescript/safe_redirect.ts
vendored
Normal 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);
|
||||
}
|
||||
14
tests/fixtures/open_redirect/typescript/safe_relative_redirect.ts
vendored
Normal file
14
tests/fixtures/open_redirect/typescript/safe_relative_redirect.ts
vendored
Normal 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);
|
||||
}
|
||||
5
tests/fixtures/open_redirect/typescript/unsafe_redirect.ts
vendored
Normal file
5
tests/fixtures/open_redirect/typescript/unsafe_redirect.ts
vendored
Normal 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);
|
||||
}
|
||||
13
tests/fixtures/prototype_pollution/full/safe_allowlist.js
vendored
Normal file
13
tests/fixtures/prototype_pollution/full/safe_allowlist.js
vendored
Normal 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;
|
||||
11
tests/fixtures/prototype_pollution/full/safe_object_create_null.js
vendored
Normal file
11
tests/fixtures/prototype_pollution/full/safe_object_create_null.js
vendored
Normal 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;
|
||||
15
tests/fixtures/prototype_pollution/full/safe_reject_list.js
vendored
Normal file
15
tests/fixtures/prototype_pollution/full/safe_reject_list.js
vendored
Normal 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;
|
||||
11
tests/fixtures/prototype_pollution/full/unsafe_dynamic_key.js
vendored
Normal file
11
tests/fixtures/prototype_pollution/full/unsafe_dynamic_key.js
vendored
Normal 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;
|
||||
19
tests/fixtures/prototype_pollution/full/unsafe_partial_null_proto.js
vendored
Normal file
19
tests/fixtures/prototype_pollution/full/unsafe_partial_null_proto.js
vendored
Normal 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;
|
||||
14
tests/fixtures/prototype_pollution/javascript/safe_bare_extend_class.js
vendored
Normal file
14
tests/fixtures/prototype_pollution/javascript/safe_bare_extend_class.js
vendored
Normal 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;
|
||||
14
tests/fixtures/prototype_pollution/javascript/safe_bare_extend_dynamic.js
vendored
Normal file
14
tests/fixtures/prototype_pollution/javascript/safe_bare_extend_dynamic.js
vendored
Normal 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;
|
||||
11
tests/fixtures/prototype_pollution/javascript/safe_lodash_merge_const.js
vendored
Normal file
11
tests/fixtures/prototype_pollution/javascript/safe_lodash_merge_const.js
vendored
Normal 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;
|
||||
9
tests/fixtures/prototype_pollution/javascript/safe_object_assign_const.js
vendored
Normal file
9
tests/fixtures/prototype_pollution/javascript/safe_object_assign_const.js
vendored
Normal 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;
|
||||
11
tests/fixtures/prototype_pollution/javascript/safe_set_value_const.js
vendored
Normal file
11
tests/fixtures/prototype_pollution/javascript/safe_set_value_const.js
vendored
Normal 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;
|
||||
14
tests/fixtures/prototype_pollution/javascript/unsafe_bare_extend_deep.js
vendored
Normal file
14
tests/fixtures/prototype_pollution/javascript/unsafe_bare_extend_deep.js
vendored
Normal 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;
|
||||
12
tests/fixtures/prototype_pollution/javascript/unsafe_dot_prop_set.js
vendored
Normal file
12
tests/fixtures/prototype_pollution/javascript/unsafe_dot_prop_set.js
vendored
Normal 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;
|
||||
12
tests/fixtures/prototype_pollution/javascript/unsafe_jsonpath_set.js
vendored
Normal file
12
tests/fixtures/prototype_pollution/javascript/unsafe_jsonpath_set.js
vendored
Normal 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;
|
||||
12
tests/fixtures/prototype_pollution/javascript/unsafe_lodash_merge.js
vendored
Normal file
12
tests/fixtures/prototype_pollution/javascript/unsafe_lodash_merge.js
vendored
Normal 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;
|
||||
8
tests/fixtures/prototype_pollution/javascript/unsafe_object_assign.js
vendored
Normal file
8
tests/fixtures/prototype_pollution/javascript/unsafe_object_assign.js
vendored
Normal 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
Loading…
Add table
Add a link
Reference in a new issue