mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
refactor(dynamic): add cross-file route detection for frameworks, enhance test coverage in PHP and Ruby
This commit is contained in:
parent
43ab4aa469
commit
0e8c900078
22 changed files with 1208 additions and 134 deletions
|
|
@ -166,6 +166,16 @@ fn class_method_python_dispatch_reads_payload_and_invokes_method() {
|
|||
assert!(h.source.contains("_nyx_resolve_annotation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_js_dispatch_builds_recursive_receiver() {
|
||||
let spec = make_spec(Lang::JavaScript);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("_nyxBuildReceiver(_Cls, 3)"));
|
||||
assert!(h.source.contains("_nyxConstructorParams"));
|
||||
assert!(h.source.contains("_nyxExportedClass"));
|
||||
assert!(h.source.contains("depth = 3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_java_emits_reflective_dispatch() {
|
||||
let spec = make_spec(Lang::Java);
|
||||
|
|
@ -285,6 +295,17 @@ mod e2e_phase_19 {
|
|||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::JavaScript,
|
||||
fixture_dir: "javascript_recursive_deps",
|
||||
vuln_file: "vuln.js",
|
||||
benign_file: "benign.js",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::TypeScript,
|
||||
fixture_dir: "typescript",
|
||||
|
|
@ -296,6 +317,17 @@ mod e2e_phase_19 {
|
|||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::TypeScript,
|
||||
fixture_dir: "typescript_recursive_deps",
|
||||
vuln_file: "vuln.ts",
|
||||
benign_file: "benign.ts",
|
||||
vuln_class: "UserService",
|
||||
benign_class: "UserService",
|
||||
method: "run",
|
||||
cap: Cap::CODE_EXEC,
|
||||
bins: &["node"],
|
||||
},
|
||||
Case {
|
||||
lang: Lang::Php,
|
||||
fixture_dir: "php",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
class ShellRunner {
|
||||
run(_command) {
|
||||
return 'safe';
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class ShellRunner {
|
||||
run(command) {
|
||||
return execSync('true ' + command).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
class ShellRunner {
|
||||
run(_command) {
|
||||
return 'safe';
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
class ShellRunner {
|
||||
run(command) {
|
||||
return execSync('true ' + command).toString();
|
||||
}
|
||||
}
|
||||
|
||||
class UserRepository {
|
||||
constructor(shellRunner) {
|
||||
this.shellRunner = shellRunner;
|
||||
}
|
||||
|
||||
find(input) {
|
||||
return this.shellRunner.run(input);
|
||||
}
|
||||
}
|
||||
|
||||
class UserService {
|
||||
constructor(userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
run(input) {
|
||||
return this.userRepository.find(input);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserService, UserRepository, ShellRunner };
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
$routes->get('users/(:num)', 'UserController::show');
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
namespace App\Controllers;
|
||||
|
||||
class UserController extends BaseController
|
||||
{
|
||||
public function show($num)
|
||||
{
|
||||
return $num;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
class UserController
|
||||
{
|
||||
public function show($id)
|
||||
{
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\UserController;
|
||||
|
||||
Route::get('/users/{id}', [UserController::class, 'show'])->middleware('auth');
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
report_show:
|
||||
path: /reports/{id}
|
||||
controller: App\Controller\ReportController::show
|
||||
methods: [POST]
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
||||
class ReportController extends AbstractController
|
||||
{
|
||||
public function show($id)
|
||||
{
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
require "hanami/action"
|
||||
|
||||
module Books
|
||||
class Show
|
||||
include Hanami::Action
|
||||
|
||||
def call(req)
|
||||
req.params[:id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Hanami.app.routes do
|
||||
get "/books/:id", to: "books.show"
|
||||
end
|
||||
|
|
@ -13,7 +13,10 @@
|
|||
|
||||
mod common;
|
||||
|
||||
use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding};
|
||||
use nyx_scanner::dynamic::framework::{
|
||||
FrameworkDetectionContext, HttpMethod, ParamSource, ProjectFileIndex, detect_binding,
|
||||
detect_binding_with_project_context,
|
||||
};
|
||||
use nyx_scanner::evidence::EntryKind;
|
||||
use nyx_scanner::summary::FuncSummary;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
|
@ -115,6 +118,30 @@ fn symfony_benign_fixture_binds_same_route_shape() {
|
|||
assert_eq!(route.path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symfony_yaml_fixture_binds_cross_file_route() {
|
||||
let path =
|
||||
"tests/dynamic_fixtures/php_frameworks/symfony_yaml/src/Controller/ReportController.php";
|
||||
let routes = "tests/dynamic_fixtures/php_frameworks/symfony_yaml/config/routes.yaml";
|
||||
let bytes = std::fs::read(path).expect("symfony yaml controller fixture exists");
|
||||
let route_bytes = std::fs::read(routes).expect("symfony yaml routes fixture exists");
|
||||
let tree = parse_php(&bytes);
|
||||
let summary = summary_for("show", path);
|
||||
let mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert("config/routes.yaml", route_bytes);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding =
|
||||
detect_binding_with_project_context(&summary, context, tree.root_node(), &bytes, Lang::Php)
|
||||
.expect("symfony adapter must bind through config/routes.yaml");
|
||||
assert_eq!(binding.adapter, "php-symfony");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/reports/{id}");
|
||||
assert_eq!(route.method, HttpMethod::POST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codeigniter_vuln_fixture_binds_route() {
|
||||
let path = "tests/dynamic_fixtures/php_frameworks/codeigniter/vuln.php";
|
||||
|
|
@ -143,6 +170,57 @@ fn codeigniter_benign_fixture_binds_same_route_shape() {
|
|||
assert_eq!(route.path, "run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_routes_fixture_binds_cross_file_route() {
|
||||
let path = "tests/dynamic_fixtures/php_frameworks/laravel_routes/app/Http/Controllers/UserController.php";
|
||||
let routes = "tests/dynamic_fixtures/php_frameworks/laravel_routes/routes/web.php";
|
||||
let bytes = std::fs::read(path).expect("laravel controller fixture exists");
|
||||
let route_bytes = std::fs::read(routes).expect("laravel routes fixture exists");
|
||||
let tree = parse_php(&bytes);
|
||||
let summary = summary_for("show", path);
|
||||
let mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert("routes/web.php", route_bytes);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding =
|
||||
detect_binding_with_project_context(&summary, context, tree.root_node(), &bytes, Lang::Php)
|
||||
.expect("laravel adapter must bind through routes/web.php");
|
||||
assert_eq!(binding.adapter, "php-laravel");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/users/{id}");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert!(
|
||||
binding.middleware.iter().any(|m| m.name == "auth"),
|
||||
"expected auth middleware from route config, got {:?}",
|
||||
binding.middleware
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codeigniter_config_fixture_binds_cross_file_route() {
|
||||
let path = "tests/dynamic_fixtures/php_frameworks/codeigniter_config/app/Controllers/UserController.php";
|
||||
let routes = "tests/dynamic_fixtures/php_frameworks/codeigniter_config/app/Config/Routes.php";
|
||||
let bytes = std::fs::read(path).expect("codeigniter controller fixture exists");
|
||||
let route_bytes = std::fs::read(routes).expect("codeigniter routes fixture exists");
|
||||
let tree = parse_php(&bytes);
|
||||
let summary = summary_for("show", path);
|
||||
let mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert("app/Config/Routes.php", route_bytes);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding =
|
||||
detect_binding_with_project_context(&summary, context, tree.root_node(), &bytes, Lang::Php)
|
||||
.expect("codeigniter adapter must bind through app/Config/Routes.php");
|
||||
assert_eq!(binding.adapter, "php-codeigniter");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "users/(:num)");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn laravel_adapter_ignores_helper_method() {
|
||||
// `helper` is declared but not referenced in any `Route::*` call.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@
|
|||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::framework::{HttpMethod, ParamSource, detect_binding};
|
||||
use nyx_scanner::dynamic::framework::{
|
||||
FrameworkDetectionContext, HttpMethod, ParamSource, ProjectFileIndex, detect_binding,
|
||||
detect_binding_with_project_context,
|
||||
};
|
||||
use nyx_scanner::evidence::EntryKind;
|
||||
use nyx_scanner::summary::FuncSummary;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
|
@ -147,6 +150,34 @@ fn hanami_benign_fixture_binds_same_route_shape() {
|
|||
assert_eq!(route.path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hanami_config_routes_fixture_binds_cross_file_route() {
|
||||
let path = "tests/dynamic_fixtures/ruby/hanami_config_routes/app/actions/books/show.rb";
|
||||
let routes = "tests/dynamic_fixtures/ruby/hanami_config_routes/config/routes.rb";
|
||||
let bytes = std::fs::read(path).expect("hanami action fixture exists");
|
||||
let route_bytes = std::fs::read(routes).expect("hanami routes fixture exists");
|
||||
let tree = parse_ruby(&bytes);
|
||||
let summary = summary_for("call", path);
|
||||
let mut project_files = ProjectFileIndex::new();
|
||||
project_files.insert("config/routes.rb", route_bytes);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: None,
|
||||
project_files: &project_files,
|
||||
};
|
||||
let binding = detect_binding_with_project_context(
|
||||
&summary,
|
||||
context,
|
||||
tree.root_node(),
|
||||
&bytes,
|
||||
Lang::Ruby,
|
||||
)
|
||||
.expect("hanami adapter must bind through config/routes.rb");
|
||||
assert_eq!(binding.adapter, "ruby-hanami");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert_eq!(route.path, "/books/:id");
|
||||
}
|
||||
|
||||
// ── Cross-adapter disambiguation ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue