mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
[pitboss] phase 09: Track J.7 + Track L.7 — OPEN_REDIRECT corpus + redirect-aware adapters
This commit is contained in:
parent
5697763f28
commit
b881af5d93
47 changed files with 2592 additions and 32 deletions
|
|
@ -28,6 +28,13 @@ pub mod php_twig;
|
|||
pub mod php_unserialize;
|
||||
pub mod python_jinja2;
|
||||
pub mod python_pickle;
|
||||
pub mod redirect_go;
|
||||
pub mod redirect_java;
|
||||
pub mod redirect_js;
|
||||
pub mod redirect_php;
|
||||
pub mod redirect_python;
|
||||
pub mod redirect_ruby;
|
||||
pub mod redirect_rust;
|
||||
pub mod ruby_erb;
|
||||
pub mod ruby_marshal;
|
||||
pub mod xpath_java;
|
||||
|
|
@ -57,6 +64,13 @@ pub use php_twig::PhpTwigAdapter;
|
|||
pub use php_unserialize::PhpUnserializeAdapter;
|
||||
pub use python_jinja2::PythonJinja2Adapter;
|
||||
pub use python_pickle::PythonPickleAdapter;
|
||||
pub use redirect_go::RedirectGoAdapter;
|
||||
pub use redirect_java::RedirectJavaAdapter;
|
||||
pub use redirect_js::RedirectJsAdapter;
|
||||
pub use redirect_php::RedirectPhpAdapter;
|
||||
pub use redirect_python::RedirectPythonAdapter;
|
||||
pub use redirect_ruby::RedirectRubyAdapter;
|
||||
pub use redirect_rust::RedirectRustAdapter;
|
||||
pub use ruby_erb::RubyErbAdapter;
|
||||
pub use ruby_marshal::RubyMarshalAdapter;
|
||||
pub use xpath_java::XpathJavaAdapter;
|
||||
|
|
|
|||
104
src/dynamic/framework/adapters/redirect_go.rs
Normal file
104
src/dynamic/framework/adapters/redirect_go.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
//! Go [`super::super::FrameworkAdapter`] matching HTTP-redirect sink
|
||||
//! constructions (`http.Redirect`, `gin.Context.Redirect`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one of
|
||||
//! the canonical Go HTTP redirect entry points and the surrounding
|
||||
//! source imports `net/http` or the gin framework.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectGoAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-go";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(last, "Redirect" | "Redirect302" | "Redirect301")
|
||||
}
|
||||
|
||||
fn source_imports_go_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"net/http",
|
||||
b"github.com/gin-gonic/gin",
|
||||
b"github.com/labstack/echo",
|
||||
b"github.com/gofiber/fiber",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectGoAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Go
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_go_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_go(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_go::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_gin_redirect() {
|
||||
let src: &[u8] = b"package vuln\n\nimport (\n\t\"net/http\"\n\t\"github.com/gin-gonic/gin\"\n)\n\
|
||||
func Run(c *gin.Context, v string) {\n\tc.Redirect(http.StatusFound, v)\n}\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("Redirect")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"package vuln\n\nfunc Add(a, b int) int { return a + b }\n";
|
||||
let tree = parse_go(src);
|
||||
let summary = FuncSummary {
|
||||
name: "Add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectGoAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
106
src/dynamic/framework/adapters/redirect_java.rs
Normal file
106
src/dynamic/framework/adapters/redirect_java.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
//! Java [`super::super::FrameworkAdapter`] matching HTTP-redirect
|
||||
//! sink constructions (`HttpServletResponse.sendRedirect`,
|
||||
//! Spring `ResponseEntity` 302 builders).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical servlet redirect entry points and the
|
||||
//! surrounding source imports a servlet API.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectJavaAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-java";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(last, "sendRedirect" | "redirect")
|
||||
}
|
||||
|
||||
fn source_imports_servlet(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"javax.servlet",
|
||||
b"jakarta.servlet",
|
||||
b"HttpServletResponse",
|
||||
b"org.springframework.http",
|
||||
b"org.springframework.web.servlet",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectJavaAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Java
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_servlet(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_java(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_java::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_send_redirect() {
|
||||
let src: &[u8] = b"import javax.servlet.http.HttpServletResponse;\n\
|
||||
class C { void run(HttpServletResponse r, String v) { r.sendRedirect(v); } }\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("sendRedirect")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"class C { int add(int a, int b) { return a + b; } }\n";
|
||||
let tree = parse_java(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectJavaAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
111
src/dynamic/framework/adapters/redirect_js.rs
Normal file
111
src/dynamic/framework/adapters/redirect_js.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! JavaScript [`super::super::FrameworkAdapter`] matching
|
||||
//! HTTP-redirect sink constructions (Express `res.redirect`,
|
||||
//! Koa `ctx.redirect`, raw Node `res.writeHead(302, { Location })`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical Node redirect entry points and the surrounding
|
||||
//! source imports the matching framework module.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectJsAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-js";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(last, "redirect" | "writeHead")
|
||||
}
|
||||
|
||||
fn source_imports_node_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"require('express')",
|
||||
b"require(\"express\")",
|
||||
b"from 'express'",
|
||||
b"from \"express\"",
|
||||
b"require('koa')",
|
||||
b"require(\"koa\")",
|
||||
b"require('http')",
|
||||
b"require(\"http\")",
|
||||
b"res.redirect",
|
||||
b"ctx.redirect",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectJsAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::JavaScript
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_node_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_express_redirect() {
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
function run(req, res, v) { res.redirect(v); }\n";
|
||||
let tree = parse_js(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("redirect")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectJsAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"function add(a, b) { return a + b; }\n";
|
||||
let tree = parse_js(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectJsAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
111
src/dynamic/framework/adapters/redirect_php.rs
Normal file
111
src/dynamic/framework/adapters/redirect_php.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! PHP [`super::super::FrameworkAdapter`] matching HTTP-redirect
|
||||
//! sink constructions (`header("Location: ...")`,
|
||||
//! Symfony `RedirectResponse`, Slim `Response::withHeader`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical PHP redirect entry points and the surrounding
|
||||
//! source imports a recognised framework / writes a `Location:`
|
||||
//! header.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectPhpAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-php";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(
|
||||
last,
|
||||
"redirect" | "withRedirect" | "RedirectResponse" | "header"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_php_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"Symfony\\Component\\HttpFoundation",
|
||||
b"Slim\\Psr7",
|
||||
b"Psr\\Http\\Message",
|
||||
b"Location:",
|
||||
b"RedirectResponse",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectPhpAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Php
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_php_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_php(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_php::LANGUAGE_PHP);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_header_location() {
|
||||
let src: &[u8] =
|
||||
b"<?php\nfunction run($v) { header(\"Location: \" . $v); exit; }\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("header")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"<?php\nfunction add($a, $b) { return $a + $b; }\n";
|
||||
let tree = parse_php(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectPhpAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
111
src/dynamic/framework/adapters/redirect_python.rs
Normal file
111
src/dynamic/framework/adapters/redirect_python.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! Python [`super::super::FrameworkAdapter`] matching HTTP-redirect
|
||||
//! sink constructions (`flask.redirect`, Django
|
||||
//! `HttpResponseRedirect`, FastAPI `RedirectResponse`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical Python web-framework redirect entry points and
|
||||
//! the surrounding source imports the matching framework module.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectPythonAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-python";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(
|
||||
last,
|
||||
"redirect" | "HttpResponseRedirect" | "RedirectResponse"
|
||||
)
|
||||
}
|
||||
|
||||
fn source_imports_python_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"from flask",
|
||||
b"import flask",
|
||||
b"from django.http",
|
||||
b"from django.shortcuts",
|
||||
b"from starlette",
|
||||
b"from fastapi.responses",
|
||||
b"from werkzeug",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectPythonAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Python
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_python_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_python(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_python::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_flask_redirect() {
|
||||
let src: &[u8] = b"from flask import redirect\n\
|
||||
def run(value):\n return redirect(value)\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("redirect")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"def add(a, b):\n return a + b\n";
|
||||
let tree = parse_python(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectPythonAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
109
src/dynamic/framework/adapters/redirect_ruby.rs
Normal file
109
src/dynamic/framework/adapters/redirect_ruby.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! Ruby [`super::super::FrameworkAdapter`] matching HTTP-redirect
|
||||
//! sink constructions (Rails `redirect_to`, Sinatra `redirect`,
|
||||
//! `Rack::Response#redirect`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical Ruby web-framework redirect entry points and
|
||||
//! the surrounding source imports / references a recognised
|
||||
//! framework module.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectRubyAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-ruby";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once('.').map(|(_, s)| s).unwrap_or(name);
|
||||
matches!(last, "redirect" | "redirect_to" | "redirect!" )
|
||||
}
|
||||
|
||||
fn source_imports_ruby_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"Rack::Response",
|
||||
b"require 'rack",
|
||||
b"require \"rack",
|
||||
b"require 'sinatra",
|
||||
b"require \"sinatra",
|
||||
b"ActionController",
|
||||
b"Rails",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectRubyAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Ruby
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_ruby_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_ruby(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_ruby::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_rack_redirect() {
|
||||
let src: &[u8] = b"require 'rack'\n\
|
||||
def run(value)\n resp = Rack::Response.new\n resp.redirect(value)\n resp\nend\n";
|
||||
let tree = parse_ruby(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("redirect")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectRubyAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"def add(a, b)\n a + b\nend\n";
|
||||
let tree = parse_ruby(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectRubyAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
110
src/dynamic/framework/adapters/redirect_rust.rs
Normal file
110
src/dynamic/framework/adapters/redirect_rust.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
//! Rust [`super::super::FrameworkAdapter`] matching HTTP-redirect
|
||||
//! sink constructions (`axum::response::Redirect::to`, actix-web
|
||||
//! `HttpResponse::Found().append_header(("Location", v))`).
|
||||
//!
|
||||
//! Phase 09 (Track J.7). Fires when the function body invokes one
|
||||
//! of the canonical Rust web-framework redirect entry points and the
|
||||
//! surrounding source imports the matching framework module.
|
||||
|
||||
use crate::dynamic::framework::{FrameworkAdapter, FrameworkBinding};
|
||||
use crate::evidence::EntryKind;
|
||||
use crate::summary::FuncSummary;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
pub struct RedirectRustAdapter;
|
||||
|
||||
const ADAPTER_NAME: &str = "redirect-rust";
|
||||
|
||||
fn callee_is_redirect(name: &str) -> bool {
|
||||
let last = name.rsplit_once("::").map(|(_, s)| s).unwrap_or(name);
|
||||
let last = last.rsplit_once('.').map(|(_, s)| s).unwrap_or(last);
|
||||
matches!(last, "to" | "redirect" | "temporary" | "permanent" | "Found")
|
||||
}
|
||||
|
||||
fn source_imports_rust_web(file_bytes: &[u8]) -> bool {
|
||||
const NEEDLES: &[&[u8]] = &[
|
||||
b"use axum::",
|
||||
b"axum::response::Redirect",
|
||||
b"use actix_web::",
|
||||
b"use rocket::",
|
||||
b"use warp::",
|
||||
b"Redirect::to",
|
||||
b"Redirect::permanent",
|
||||
b"Redirect::temporary",
|
||||
];
|
||||
NEEDLES
|
||||
.iter()
|
||||
.any(|n| file_bytes.windows(n.len()).any(|w| w == *n))
|
||||
}
|
||||
|
||||
impl FrameworkAdapter for RedirectRustAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
ADAPTER_NAME
|
||||
}
|
||||
|
||||
fn lang(&self) -> Lang {
|
||||
Lang::Rust
|
||||
}
|
||||
|
||||
fn detect(
|
||||
&self,
|
||||
summary: &FuncSummary,
|
||||
_ast: tree_sitter::Node<'_>,
|
||||
file_bytes: &[u8],
|
||||
) -> Option<FrameworkBinding> {
|
||||
let matches_call = super::any_callee_matches(summary, callee_is_redirect);
|
||||
let matches_source = source_imports_rust_web(file_bytes);
|
||||
if matches_call && matches_source {
|
||||
Some(FrameworkBinding {
|
||||
adapter: ADAPTER_NAME.to_owned(),
|
||||
kind: EntryKind::Function,
|
||||
route: None,
|
||||
request_params: Vec::new(),
|
||||
response_writer: None,
|
||||
middleware: Vec::new(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_rust(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fires_on_axum_redirect_to() {
|
||||
let src: &[u8] =
|
||||
b"use axum::response::Redirect;\n\nfn run(v: String) -> Redirect { Redirect::to(&v) }\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "run".into(),
|
||||
callees: vec![crate::summary::CalleeSite::bare("to")],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_plain_function() {
|
||||
let src: &[u8] = b"fn add(a: i32, b: i32) -> i32 { a + b }\n";
|
||||
let tree = parse_rust(src);
|
||||
let summary = FuncSummary {
|
||||
name: "add".into(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(RedirectRustAdapter
|
||||
.detect(&summary, tree.root_node(), src)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue