refactor(dynamic): replace PHP route stubs with framework-aware route replay logic for Laravel and Symfony, enhance helper functions, and update related test fixtures

This commit is contained in:
elipeter 2026-05-26 14:19:01 -05:00
parent aaf49acefb
commit ed398e2834
14 changed files with 835 additions and 345 deletions

View file

@ -433,8 +433,13 @@ fn visit_laravel_routes<'a>(
if out.is_some() {
return;
}
if node.kind() == "scoped_call_expression"
&& let Some(found) = try_laravel_route(node, bytes, target, controller)
if node.kind() == "scoped_call_expression" {
if let Some(found) = try_laravel_route(node, bytes, target, controller) {
*out = Some(found);
return;
}
} else if node.kind() == "member_call_expression"
&& let Some(found) = try_laravel_member_route(node, bytes, target, controller)
{
*out = Some(found);
return;
@ -470,6 +475,30 @@ fn try_laravel_route<'a>(
})
}
fn try_laravel_member_route<'a>(
call: Node<'a>,
bytes: &'a [u8],
target: &str,
controller: Option<&str>,
) -> Option<RouteShape> {
let object = call.child_by_field_name("object")?.utf8_text(bytes).ok()?;
if object.trim_start_matches('$').trim() != "router" {
return None;
}
let verb_node = call.child_by_field_name("name")?.utf8_text(bytes).ok()?;
let args = call.child_by_field_name("arguments")?;
let methods = laravel_route_methods(verb_node, args, bytes)?;
let path = laravel_route_path(verb_node, args, bytes)?;
if !laravel_callable_matches(verb_node, args, bytes, target, controller) {
return None;
}
Some(if methods.len() > 1 {
RouteShape::multi(methods, path)
} else {
RouteShape::single(methods[0], path)
})
}
fn laravel_route_methods(verb: &str, arguments: Node<'_>, bytes: &[u8]) -> Option<Vec<HttpMethod>> {
match verb {
"any" => Some(vec![

View file

@ -104,6 +104,7 @@ fn stage_harness(
// Copy the entry file into the workdir so the harness can import/include it.
copy_entry_file(spec, &workdir, harness_src.entry_subpath.as_deref());
copy_java_sibling_sources(spec, &workdir);
copy_php_project_manifests(spec, &workdir);
Ok(workdir)
}
@ -259,6 +260,26 @@ fn copy_java_sibling_sources(spec: &HarnessSpec, workdir: &Path) {
}
}
fn copy_php_project_manifests(spec: &HarnessSpec, workdir: &Path) {
if spec.lang != crate::symbol::Lang::Php {
return;
}
let entry = PathBuf::from(&spec.entry_file);
let mut dir = entry.parent();
while let Some(current) = dir {
let composer_json = current.join("composer.json");
if composer_json.exists() {
let _ = fs::copy(&composer_json, workdir.join("composer.json"));
let composer_lock = current.join("composer.lock");
if composer_lock.exists() {
let _ = fs::copy(composer_lock, workdir.join("composer.lock"));
}
return;
}
dir = current.parent();
}
}
/// Extract the source of the entry file (for repro bundles). Best-effort.
fn extract_entry_source(spec: &HarnessSpec) -> String {
let candidates = [

View file

@ -143,22 +143,21 @@ pub enum PhpShape {
/// stub (query/body) and invokes the closure resolved from the
/// global (which the entry file publishes during include).
RouteClosure,
/// Laravel route — `Route::get('/x', 'Controller@method')` or
/// closure callable. Phase 16 v1 dispatches through the same
/// `$GLOBALS['__nyx_route']` channel as `RouteClosure` but
/// publishes a `NYX_LARAVEL_TEST=1` stdout marker so the
/// verifier can confirm the framework toolchain knob propagated.
/// Laravel route — `Route::get('/x', 'Controller@method')`,
/// `$router->get('/x', [Controller::class, 'method'])`, or
/// closure callable. The harness requires Composer autoload,
/// registers the fixture routes against `Illuminate\Routing\Router`,
/// and dispatches an `Illuminate\Http\Request`.
LaravelRoute,
/// Symfony route — `#[Route('/x')]` PHP attribute on a
/// controller method or top-level function. Phase 16 v1
/// dispatches via reflective invocation (the entry file's
/// `entry.php` instantiates the controller class and the harness
/// calls the method) plus an `NYX_SYMFONY_TEST=1` stdout marker.
/// controller method or top-level function. The harness requires
/// Composer autoload, registers the fixture routes into a
/// `RouteCollection`, and drives `HttpKernel::handle`.
SymfonyRoute,
/// CodeIgniter route — `$routes->get('users/(:num)', ...)`
/// published from `app/Config/Routes.php`. Phase 16 v1
/// dispatches via the `$GLOBALS['__nyx_route']` channel plus a
/// `NYX_CODEIGNITER_TEST=1` stdout marker.
/// published from `app/Config/Routes.php`. The harness requires
/// Composer autoload, registers routes against CodeIgniter's
/// `RouteCollection`, and replays the matching route handler.
CodeIgniterRoute,
/// CLI script driven by `$argv`. Harness mutates `$argv` then
/// includes the entry file (whose top-level body reads `$argv`),
@ -1953,6 +1952,7 @@ fn generate_source(spec: &HarnessSpec, shape: PhpShape) -> String {
let shim = probe_shim();
let toolchain_marker = build_toolchain_marker(shape);
let route_methods_fn = build_route_methods_fn(spec);
let framework_helpers = build_framework_helpers(spec, shape);
let crash_callee = if entry_fn.is_empty() {
"main"
} else {
@ -1976,9 +1976,10 @@ function nyx_payload(): string {{
return '';
}}
{route_methods_fn}
$payload = nyx_payload();
{route_methods_fn}
{framework_helpers}
$payload = nyx_payload();
// Phase 08 sink-site signal handler: install AFTER payload decode so a crash
// inside `nyx_payload` writes no Crash probe and routes the verifier to
@ -2016,10 +2017,286 @@ foreach (__nyx_route_methods() as $__nyx_method) {{
shim = shim,
toolchain_marker = toolchain_marker,
route_methods_fn = route_methods_fn,
framework_helpers = framework_helpers,
crash_callee = crash_callee,
)
}
fn route_path_for_spec(spec: &HarnessSpec) -> String {
spec.framework
.as_ref()
.and_then(|binding| binding.route.as_ref())
.map(|route| route.path.clone())
.unwrap_or_else(|| {
if matches!(spec.entry_kind, crate::evidence::EntryKind::HttpRoute) {
"/run/{payload}".to_owned()
} else {
"/".to_owned()
}
})
}
fn build_framework_helpers(spec: &HarnessSpec, shape: PhpShape) -> String {
if !matches!(
shape,
PhpShape::LaravelRoute | PhpShape::SymfonyRoute | PhpShape::CodeIgniterRoute
) {
return String::new();
}
let route_path = php_string_literal(&route_path_for_spec(spec));
let mut out = String::new();
out.push_str("const __NYX_ROUTE_PATH = ");
out.push_str(&route_path);
out.push_str(";\n");
out.push_str(
r#"
function __nyx_find_user_function(string $leaf): ?string {
$funcs = get_defined_functions();
foreach (($funcs['user'] ?? []) as $fn) {
if ($fn === $leaf || str_ends_with($fn, '\\' . $leaf)) {
return $fn;
}
}
return null;
}
function __nyx_request_path(string $template, string $payload): string {
$encoded = rawurlencode($payload);
$path = $template;
$replacements = [
'{payload}' => $encoded,
'{payload?}' => $encoded,
'(:any)' => $encoded,
'(:segment)' => $encoded,
'(:alphanum)' => $encoded,
'(:alpha)' => $encoded,
'(:num)' => $encoded,
'(:hash)' => $encoded,
];
foreach ($replacements as $needle => $value) {
if (str_contains($path, $needle)) {
$path = str_replace($needle, $value, $path);
break;
}
}
if ($path === '') {
$path = '/';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
return $path;
}
function __nyx_response_text($response): string {
if ($response === null) {
return '';
}
if (is_string($response)) {
return $response;
}
if (is_object($response)) {
if (method_exists($response, 'getContent')) {
return (string) $response->getContent();
}
if (method_exists($response, 'getBody')) {
return (string) $response->getBody();
}
if (method_exists($response, '__toString')) {
return (string) $response;
}
}
if (is_array($response)) {
return json_encode($response) ?: '';
}
return (string) $response;
}
function __nyx_require_registrar(): string {
$registrar = __nyx_find_user_function('nyx_register_routes');
if ($registrar === null) {
throw new RuntimeException('NYX_ROUTE_REGISTRAR_MISSING: nyx_register_routes');
}
return $registrar;
}
"#,
);
match shape {
PhpShape::LaravelRoute => out.push_str(
r#"
function __nyx_dispatch_laravel(string $payload, string $method) {
$required = [
'\Illuminate\Container\Container',
'\Illuminate\Events\Dispatcher',
'\Illuminate\Http\Request',
'\Illuminate\Routing\Router',
];
foreach ($required as $class) {
if (!class_exists($class)) {
throw new RuntimeException('NYX_LARAVEL_CLASS_MISSING: ' . $class);
}
}
$container = new \Illuminate\Container\Container();
\Illuminate\Container\Container::setInstance($container);
$events = new \Illuminate\Events\Dispatcher($container);
$router = new \Illuminate\Routing\Router($events, $container);
$registrar = __nyx_require_registrar();
$registrar($router);
$path = __nyx_request_path(__NYX_ROUTE_PATH, $payload);
$request = \Illuminate\Http\Request::create($path, $method, ['payload' => $payload], [], [], [], $payload);
$response = $router->dispatch($request);
return __nyx_response_text($response);
}
"#,
),
PhpShape::SymfonyRoute => out.push_str(
r#"
function __nyx_dispatch_symfony(string $payload, string $method) {
$required = [
'\Symfony\Component\EventDispatcher\EventDispatcher',
'\Symfony\Component\HttpFoundation\Request',
'\Symfony\Component\HttpFoundation\RequestStack',
'\Symfony\Component\HttpKernel\Controller\ArgumentResolver',
'\Symfony\Component\HttpKernel\Controller\ControllerResolver',
'\Symfony\Component\HttpKernel\HttpKernel',
'\Symfony\Component\Routing\Matcher\UrlMatcher',
'\Symfony\Component\Routing\RequestContext',
'\Symfony\Component\Routing\RouteCollection',
];
foreach ($required as $class) {
if (!class_exists($class)) {
throw new RuntimeException('NYX_SYMFONY_CLASS_MISSING: ' . $class);
}
}
$routes = new \Symfony\Component\Routing\RouteCollection();
$registrar = __nyx_require_registrar();
$registrar($routes);
$path = __nyx_request_path(__NYX_ROUTE_PATH, $payload);
$request = \Symfony\Component\HttpFoundation\Request::create($path, $method, ['payload' => $payload], [], [], [], $payload);
$context = new \Symfony\Component\Routing\RequestContext();
$context->fromRequest($request);
$matcher = new \Symfony\Component\Routing\Matcher\UrlMatcher($routes, $context);
$request->attributes->add($matcher->match($request->getPathInfo()));
$kernel = new \Symfony\Component\HttpKernel\HttpKernel(
new \Symfony\Component\EventDispatcher\EventDispatcher(),
new \Symfony\Component\HttpKernel\Controller\ControllerResolver(),
new \Symfony\Component\HttpFoundation\RequestStack(),
new \Symfony\Component\HttpKernel\Controller\ArgumentResolver()
);
$response = $kernel->handle($request);
return __nyx_response_text($response);
}
"#,
),
PhpShape::CodeIgniterRoute => out.push_str(
r#"
function __nyx_define_codeigniter_config(): void {
if (!defined('ENVIRONMENT')) define('ENVIRONMENT', 'testing');
if (!defined('ROOTPATH')) define('ROOTPATH', __DIR__ . DIRECTORY_SEPARATOR);
if (!defined('APPPATH')) define('APPPATH', __DIR__ . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR);
if (!defined('WRITEPATH')) define('WRITEPATH', __DIR__ . DIRECTORY_SEPARATOR . 'writable' . DIRECTORY_SEPARATOR);
if (!defined('SYSTEMPATH')) define('SYSTEMPATH', __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'codeigniter4' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'system' . DIRECTORY_SEPARATOR);
if (!is_dir(APPPATH . 'Config')) @mkdir(APPPATH . 'Config', 0777, true);
if (!is_dir(WRITEPATH)) @mkdir(WRITEPATH, 0777, true);
if (!class_exists('Config\\Modules') && class_exists('\\CodeIgniter\\Config\\Modules')) {
eval('namespace Config; class Modules extends \\CodeIgniter\\Config\\Modules {}');
}
if (!class_exists('Config\\Routing') && class_exists('\\CodeIgniter\\Config\\Routing')) {
eval('namespace Config; class Routing extends \\CodeIgniter\\Config\\Routing {}');
}
if (!class_exists('Config\\App') && class_exists('\\CodeIgniter\\Config\\BaseConfig')) {
eval('namespace Config; class App extends \\CodeIgniter\\Config\\BaseConfig { public string $baseURL = "http://localhost/"; public array $supportedLocales = ["en"]; }');
}
if (!class_exists('Config\\Services') && class_exists('\\CodeIgniter\\Config\\BaseService')) {
eval('namespace Config; class Services extends \\CodeIgniter\\Config\\BaseService {}');
}
}
function __nyx_codeigniter_routes() {
__nyx_define_codeigniter_config();
if (!class_exists('\\CodeIgniter\\Router\\RouteCollection')) {
throw new RuntimeException('NYX_CODEIGNITER_CLASS_MISSING: \\CodeIgniter\\Router\\RouteCollection');
}
if (class_exists('\\CodeIgniter\\Config\\Services')) {
try {
$routes = \CodeIgniter\Config\Services::routes(false);
if ($routes !== null) {
return $routes;
}
} catch (Throwable $_) {
}
}
$modules = class_exists('Config\\Modules') ? new \Config\Modules() : null;
$routing = class_exists('Config\\Routing') ? new \Config\Routing() : null;
if ($routing !== null) {
$routing->defaultNamespace = 'App\\Controllers';
$routing->defaultController = 'Home';
$routing->defaultMethod = 'index';
$routing->translateURIDashes = false;
$routing->autoRoute = false;
$routing->routeFiles = [];
}
$locator = class_exists('\\CodeIgniter\\Autoloader\\FileLocator') && $modules !== null
? new \CodeIgniter\Autoloader\FileLocator($modules)
: null;
return new \CodeIgniter\Router\RouteCollection($locator, $modules, $routing);
}
function __nyx_ci_pattern_regex(string $pattern): string {
$regex = str_replace(
['(:any)', '(:segment)', '(:alphanum)', '(:alpha)', '(:num)', '(:hash)'],
['(.+)', '([^/]+)', '([a-zA-Z0-9]+)', '([a-zA-Z]+)', '([0-9]+)', '([^/]+)'],
$pattern
);
return '#^' . str_replace('#', '\\#', trim($regex, '/')) . '$#u';
}
function __nyx_ci_invoke_handler($handler, array $args, string $payload) {
if (is_callable($handler)) {
return call_user_func_array($handler, $args ?: [$payload]);
}
if (!is_string($handler)) {
throw new RuntimeException('NYX_CODEIGNITER_HANDLER_UNSUPPORTED');
}
[$target, $_tail] = array_pad(explode('/', $handler, 2), 2, '');
if (!str_contains($target, '::')) {
throw new RuntimeException('NYX_CODEIGNITER_HANDLER_BAD: ' . $handler);
}
[$class, $method] = explode('::', $target, 2);
if (!class_exists($class) && class_exists('App\\Controllers\\' . ltrim($class, '\\'))) {
$class = 'App\\Controllers\\' . ltrim($class, '\\');
}
$controller = new $class();
return call_user_func_array([$controller, $method], $args ?: [$payload]);
}
function __nyx_dispatch_codeigniter(string $payload, string $method) {
$routes = __nyx_codeigniter_routes();
$registrar = __nyx_require_registrar();
$registrar($routes);
if (method_exists($routes, 'setHTTPVerb')) {
$routes->setHTTPVerb($method);
}
$path = ltrim(__nyx_request_path(__NYX_ROUTE_PATH, $payload), '/');
$map = method_exists($routes, 'getRoutes') ? $routes->getRoutes($method) : [];
foreach ($map as $pattern => $handler) {
if (preg_match(__nyx_ci_pattern_regex((string) $pattern), $path, $matches)) {
array_shift($matches);
$decoded = array_map('rawurldecode', $matches);
return __nyx_response_text(__nyx_ci_invoke_handler($handler, $decoded, $payload));
}
}
throw new RuntimeException('NYX_CODEIGNITER_ROUTE_NOT_FOUND: ' . $method . ' ' . $path);
}
"#,
),
_ => {}
}
out
}
fn build_route_methods_fn(spec: &HarnessSpec) -> String {
let mut methods = spec
.framework
@ -2100,14 +2377,30 @@ fn build_pre_call(spec: &HarnessSpec, shape: PhpShape) -> String {
out
}
fn build_entry_block(_shape: PhpShape) -> String {
r#"try {
fn build_entry_block(shape: PhpShape) -> String {
let autoload = if matches!(
shape,
PhpShape::LaravelRoute | PhpShape::SymfonyRoute | PhpShape::CodeIgniterRoute
) {
r#"$__nyx_autoload = __DIR__ . '/vendor/autoload.php';
if (!is_file($__nyx_autoload)) {
fwrite(STDERR, 'NYX_IMPORT_ERROR: missing Composer autoload; run composer install for framework route replay' . "\n");
exit(77);
}
require_once $__nyx_autoload;
"#
} else {
""
};
format!(
r#"{autoload}try {{
require_once __DIR__ . '/entry.php';
} catch (Throwable $e) {
}} catch (Throwable $e) {{
fwrite(STDERR, 'NYX_IMPORT_ERROR: ' . $e->getMessage() . "\n");
exit(77);
}"#
.to_owned()
}}"#,
autoload = autoload
)
}
/// Phase 11 (Track J.9) CRYPTO harness for PHP.
@ -2899,7 +3192,7 @@ fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
"null".to_owned()
}
}
PhpShape::RouteClosure | PhpShape::LaravelRoute | PhpShape::CodeIgniterRoute => {
PhpShape::RouteClosure => {
// Entry script publishes the route closure via
// `$GLOBALS['__nyx_route']`. When the global is missing,
// fall back to calling the named function directly.
@ -2907,17 +3200,10 @@ fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
"(isset($GLOBALS['__nyx_route']) && is_callable($GLOBALS['__nyx_route'])) ? call_user_func($GLOBALS['__nyx_route'], $payload) : (function_exists({func:?}) ? {func}($payload) : null)"
)
}
PhpShape::SymfonyRoute => {
// Symfony controllers are normally reached through
// `HttpKernel::handle`. The Phase 16 v1 harness drives
// the action directly: the entry file publishes a
// controller instance via `$GLOBALS['__nyx_controller']`
// and the harness reflectively invokes the action method.
// Falls back to calling a bare function when no
// controller class was published.
format!(
"(isset($GLOBALS['__nyx_controller']) && is_object($GLOBALS['__nyx_controller'])) ? $GLOBALS['__nyx_controller']->{func}($payload) : (function_exists({func:?}) ? {func}($payload) : null)"
)
PhpShape::LaravelRoute => "__nyx_dispatch_laravel($payload, $__nyx_method)".to_owned(),
PhpShape::SymfonyRoute => "__nyx_dispatch_symfony($payload, $__nyx_method)".to_owned(),
PhpShape::CodeIgniterRoute => {
"__nyx_dispatch_codeigniter($payload, $__nyx_method)".to_owned()
}
PhpShape::Generic => build_generic_call(spec, func),
}
@ -3118,7 +3404,9 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
let src = generate_source(&spec, PhpShape::LaravelRoute);
assert!(src.contains("NYX_LARAVEL_TEST=1"));
assert!(src.contains("$GLOBALS['__nyx_route']"));
assert!(src.contains("vendor/autoload.php"));
assert!(src.contains("__nyx_dispatch_laravel($payload, $__nyx_method)"));
assert!(!src.contains("$GLOBALS['__nyx_route']"));
}
#[test]
@ -3146,8 +3434,10 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
let src = generate_source(&spec, PhpShape::SymfonyRoute);
assert!(src.contains("NYX_SYMFONY_TEST=1"));
assert!(src.contains("$GLOBALS['__nyx_controller']"));
assert!(src.contains("->run($payload)"));
assert!(src.contains("vendor/autoload.php"));
assert!(src.contains("Symfony\\Component\\HttpKernel\\HttpKernel"));
assert!(src.contains("__nyx_dispatch_symfony($payload, $__nyx_method)"));
assert!(!src.contains("$GLOBALS['__nyx_controller']"));
}
#[test]
@ -3155,7 +3445,10 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "entry.php");
let src = generate_source(&spec, PhpShape::CodeIgniterRoute);
assert!(src.contains("NYX_CODEIGNITER_TEST=1"));
assert!(src.contains("$GLOBALS['__nyx_route']"));
assert!(src.contains("vendor/autoload.php"));
assert!(src.contains("CodeIgniter\\Router\\RouteCollection"));
assert!(src.contains("__nyx_dispatch_codeigniter($payload, $__nyx_method)"));
assert!(!src.contains("$GLOBALS['__nyx_route']"));
}
#[test]

View file

@ -544,10 +544,14 @@ impl RustShape {
let has_actix_strong = source.contains("use actix_web")
|| source.contains("actix_web::")
|| source.contains("// nyx-shape: actix");
let has_axum_strong = source.contains("use axum::")
let has_axum_import = source.contains("use axum::")
|| source.contains("axum::Router")
|| source.contains("axum::routing");
let has_axum_route = source.contains("axum::Router")
|| source.contains("Router::new")
|| source.contains("axum::routing")
|| source.contains("// nyx-shape: axum");
|| source.contains("// nyx-shape: axum")
|| source.contains("// nyx-shape: axum-route");
let has_attribute_route = source.contains("#[get(")
|| source.contains("#[post(")
|| source.contains("#[put(")
@ -578,13 +582,14 @@ impl RustShape {
Self::ActixWebRoute
};
}
if has_axum_strong {
if has_axum_route {
return Self::AxumRoute;
}
// Legacy weak detectors: HttpResponse / IntoResponse may
// appear in code that does not import a known framework.
let has_actix_weak = source.contains("HttpResponse") || source.contains("HttpRequest");
let has_axum_weak = source.contains("IntoResponse")
let has_axum_weak = has_axum_import
|| source.contains("IntoResponse")
|| source.contains("Json(")
|| source.contains("Query(");
if has_axum_weak {
@ -2018,7 +2023,7 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
}
let cargo_toml = generate_cargo_toml(spec.expected_cap);
let cargo_toml = generate_cargo_toml_for_shape(spec.expected_cap, shape);
let main_rs = generate_main_rs(spec, shape);
Ok(HarnessSource {
@ -2045,6 +2050,7 @@ fn emit_class_method_harness(spec: &HarnessSpec, class: &str, method: &str) -> H
let entry_label = format!("{class}::{method}");
let entry_src = read_entry_source(&spec.entry_file);
let receiver_expr = rust_receiver_expr(&entry_src, class, 3);
let method_invocation = rust_class_method_invocation(&entry_src, class, method);
let body = format!(
r#"//! Nyx dynamic harness — class method (Phase 19 / Track M.1).
mod entry;
@ -2054,7 +2060,7 @@ fn main() {{
let _ = &payload;
__nyx_install_crash_guard("{entry_label}");
let instance = {receiver_expr};
let _ = instance.{method}(&payload);
{method_invocation}
println!("__NYX_SINK_HIT__");
}}
@ -2100,9 +2106,9 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
Some(out)
}}
"#,
method = method,
entry_label = entry_label,
receiver_expr = receiver_expr,
method_invocation = method_invocation,
);
HarnessSource {
source: body,
@ -2147,6 +2153,76 @@ fn class_has_new(entry_src: &str, class: &str) -> bool {
}
}
fn rust_class_method_invocation(entry_src: &str, class: &str, method: &str) -> String {
if rust_method_returns_printable_stdout(entry_src, class, method) {
format!(
"let __nyx_result = instance.{method}(&payload);\n print!(\"{{}}\", __nyx_result);"
)
} else {
format!("let _ = instance.{method}(&payload);")
}
}
fn rust_method_returns_printable_stdout(entry_src: &str, class: &str, method: &str) -> bool {
let impl_marker = format!("impl {class}");
let mut search_from = 0usize;
while let Some(rel) = entry_src[search_from..].find(&impl_marker) {
let impl_start = search_from + rel;
let after_class = impl_start + impl_marker.len();
if entry_src[after_class..]
.chars()
.next()
.is_some_and(is_ident_char)
{
search_from = after_class;
continue;
}
let Some(open_rel) = entry_src[after_class..].find('{') else {
return false;
};
let block_start = after_class + open_rel;
let Some(block) = balanced_block(&entry_src[block_start..]) else {
return false;
};
if method_body_returns_printable_stdout(&block[1..block.len() - 1], method) {
return true;
}
search_from = block_start + block.len();
}
false
}
fn method_body_returns_printable_stdout(impl_body: &str, method: &str) -> bool {
let method_marker = format!("fn {method}");
let mut search_from = 0usize;
while let Some(rel) = impl_body[search_from..].find(&method_marker) {
let method_start = search_from + rel;
let after_name = method_start + method_marker.len();
if impl_body[after_name..]
.chars()
.next()
.is_some_and(is_ident_char)
{
search_from = after_name;
continue;
}
let Some(body_open_rel) = impl_body[after_name..].find('{') else {
return false;
};
let sig = &impl_body[after_name..after_name + body_open_rel];
if let Some(ret) = sig.split("->").nth(1) {
let ret = ret.trim();
return ret.starts_with("String")
|| ret.starts_with("std::string::String")
|| ret.starts_with("&str")
|| ret.starts_with("&'static str")
|| ret.starts_with("& 'static str");
}
search_from = after_name;
}
false
}
fn rust_struct_literal(entry_src: &str, class: &str, depth: usize) -> Option<String> {
if depth == 0 {
return None;
@ -2456,6 +2532,10 @@ fn word_in_text(text: &str, kw: &str) -> bool {
false
}
fn is_ident_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}
/// Generate `Cargo.toml` for the harness crate.
///
/// Dependencies are driven by `expected_cap`:
@ -2470,6 +2550,27 @@ pub fn generate_cargo_toml(cap: Cap) -> String {
generate_cargo_toml_with_extras(cap, false)
}
fn generate_cargo_toml_for_shape(cap: Cap, shape: RustShape) -> String {
let mut cargo = generate_cargo_toml_with_extras(cap, false);
let deps = match shape {
RustShape::AxumRoute => Some(
"axum = \"0.7\"\nserde = { version = \"1\", features = [\"derive\"] }\ntokio = { version = \"1\", features = [\"full\"] }\ntower = { version = \"0.5\", features = [\"util\"] }\n",
),
RustShape::ActixRoute => {
Some("actix-web = \"4\"\nserde = { version = \"1\", features = [\"derive\"] }\n")
}
RustShape::RocketRoute => Some("rocket = \"0.5\"\n"),
RustShape::WarpRoute => Some(
"serde = { version = \"1\", features = [\"derive\"] }\ntokio = { version = \"1\", features = [\"full\"] }\nwarp = \"0.3\"\n",
),
_ => None,
};
if let Some(deps) = deps {
cargo.push_str(deps);
}
cargo
}
/// Variant of [`generate_cargo_toml`] that conditionally pulls in
/// `percent-encoding` for the HEADER_INJECTION benign control fixture
/// (it routes the value through `utf8_percent_encode` to land CRLF as
@ -2548,6 +2649,44 @@ fn nyx_payload() -> String {{
String::new()
}}
fn nyx_route_uri(path: &str, query: Option<&str>, payload: &str) -> String {{
let mut uri = if path.starts_with('/') {{
path.to_owned()
}} else {{
format!("/{{path}}")
}};
if let Some(name) = query {{
uri.push('?');
uri.push_str(name);
uri.push('=');
uri.push_str(&nyx_url_encode(payload));
}}
uri
}}
fn nyx_route_payload(payload: &str) -> String {{
let trimmed = payload.trim_start();
if trimmed.starts_with(';') || trimmed.starts_with("&&") || trimmed.starts_with("||") {{
format!("true {{payload}}")
}} else {{
payload.to_owned()
}}
}}
fn nyx_url_encode(input: &str) -> String {{
let mut out = String::with_capacity(input.len());
const HEX: &[u8; 16] = b"0123456789ABCDEF";
for b in input.bytes() {{
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {{
out.push(b as char);
}} else {{
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}}
}}
out
}}
/// Minimal base64 decoder (no external deps).
fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
const TABLE: [u8; 128] = {{
@ -2604,25 +2743,119 @@ fn build_call(spec: &HarnessSpec, func: &str, shape: RustShape) -> (String, Stri
}
RustShape::ActixWebRoute => actix_invocation(spec, func),
RustShape::AxumHandler => axum_invocation(spec, func),
// Phase 17 framework dispatchers. Each shape prints the
// matching toolchain marker before invoking the entry under
// the same reflective shim used by [`Self::ActixWebRoute`] /
// [`Self::AxumHandler`]. Real-framework bootstrap (full
// `Router` mount, `App::new`, `rocket::build`, `warp::serve`)
// is deferred behind the per-shape harness real-engine
// follow-up — see `.pitboss/play/deferred.md`.
RustShape::ActixRoute => framework_route_invocation(spec, func, "NYX_ACTIX_TEST=1"),
RustShape::AxumRoute => framework_route_invocation(spec, func, "NYX_AXUM_TEST=1"),
RustShape::RocketRoute => framework_route_invocation(spec, func, "NYX_ROCKET_TEST=1"),
RustShape::WarpRoute => framework_route_invocation(spec, func, "NYX_WARP_TEST=1"),
RustShape::ActixRoute => actix_route_invocation(spec, func),
RustShape::AxumRoute => axum_route_invocation(spec, func),
RustShape::RocketRoute => rocket_route_invocation(spec, func),
RustShape::WarpRoute => warp_route_invocation(spec, func),
RustShape::ClapCli => clap_invocation(spec, func),
}
}
fn framework_route_invocation(spec: &HarnessSpec, func: &str, marker: &str) -> (String, String) {
let pre = format!(" println!(\"{marker}\");\n");
let (inner_pre, call) = actix_invocation(spec, func);
(format!("{pre}{inner_pre}"), call)
fn framework_route_uri_expr(spec: &HarnessSpec) -> String {
let path = spec
.framework
.as_ref()
.and_then(|binding| binding.route.as_ref())
.map(|route| route.path.as_str())
.unwrap_or("/run");
match &spec.payload_slot {
PayloadSlot::QueryParam(name) => {
let payload_expr = if spec.expected_cap.contains(Cap::CODE_EXEC) {
"&nyx_route_payload(&payload)"
} else {
"&payload"
};
format!(
"nyx_route_uri({}, Some({}), {})",
rust_string_literal(path),
rust_string_literal(name),
payload_expr
)
}
_ => format!(
"nyx_route_uri({}, None, &payload)",
rust_string_literal(path)
),
}
}
fn axum_route_invocation(spec: &HarnessSpec, _func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_AXUM_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("tokio runtime");
__nyx_rt.block_on(async {{
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
let app = entry::build();
let uri = {uri};
let request = Request::builder()
.method("GET")
.uri(uri)
.body(Body::empty())
.expect("axum request");
let _ = app.oneshot(request).await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn actix_route_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_ACTIX_TEST=1\");\n".to_owned(),
format!(
r#"actix_web::rt::System::new().block_on(async {{
let app = actix_web::test::init_service(actix_web::App::new().service(entry::{func})).await;
let uri = {uri};
let req = actix_web::test::TestRequest::get().uri(&uri).to_request();
let _ = actix_web::test::call_service(&app, req).await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn rocket_route_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_ROCKET_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = rocket::tokio::runtime::Builder::new_current_thread().enable_all().build().expect("rocket runtime");
__nyx_rt.block_on(async {{
let rocket = rocket::build().mount("/", rocket::routes![entry::{func}]);
let client = rocket::local::asynchronous::Client::tracked(rocket)
.await
.expect("rocket client");
let uri = {uri};
let _ = client.get(uri).dispatch().await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn warp_route_invocation(spec: &HarnessSpec, _func: &str) -> (String, String) {
let uri = framework_route_uri_expr(spec);
(
" println!(\"NYX_WARP_TEST=1\");\n".to_owned(),
format!(
r#"let __nyx_rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("tokio runtime");
__nyx_rt.block_on(async {{
let filter = entry::build();
let uri = {uri};
let _ = warp::test::request()
.method("GET")
.path(&uri)
.reply(&filter)
.await;
}});
println!("__NYX_SINK_HIT__");"#
),
)
}
fn actix_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
@ -2787,6 +3020,35 @@ mod tests {
assert!(!class_derives_default(src, "UserService"));
}
#[test]
fn class_method_string_return_is_printed_for_oracle_visibility() {
let src = r#"
pub struct UserService;
impl UserService {
pub fn run(&self, input: &str) -> String {
input.to_owned()
}
}
"#;
let invocation = rust_class_method_invocation(src, "UserService", "run");
assert!(invocation.contains("let __nyx_result = instance.run(&payload);"));
assert!(invocation.contains("print!(\"{}\", __nyx_result);"));
}
#[test]
fn class_method_void_return_keeps_direct_invocation() {
let src = r#"
pub struct UserService;
impl UserService {
pub fn run(&self, input: &str) {
let _ = input;
}
}
"#;
let invocation = rust_class_method_invocation(src, "UserService", "run");
assert_eq!(invocation, "let _ = instance.run(&payload);");
}
#[test]
fn emit_env_var_slot() {
let spec = make_spec(PayloadSlot::EnvVar("NYX_INPUT".into()));
@ -2838,14 +3100,19 @@ mod tests {
#[test]
fn shape_detect_axum_handler() {
// Phase 17 — Track L.15: a strong `use axum::` import now
// routes to the framework-aware [`RustShape::AxumRoute`]
// shape; the legacy [`RustShape::AxumHandler`] fires only on
// weak detectors (`IntoResponse` / `Json(` without `use
// axum::`).
// Importing an extractor is a handler hint, not proof that the
// fixture exports an app builder. Native Axum request replay
// requires a router shape.
let src =
"use axum::extract::Query; pub fn handler(payload: &str) -> String { String::new() }";
let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
assert_eq!(RustShape::detect(&spec, src), RustShape::AxumHandler);
}
#[test]
fn shape_detect_axum_router() {
let src = "use axum::Router; use axum::routing::get; pub async fn run() -> String { String::new() } pub fn build() -> Router { Router::new().route(\"/run\", get(run)) }";
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
assert_eq!(RustShape::detect(&spec, src), RustShape::AxumRoute);
}
@ -2953,6 +3220,7 @@ mod tests {
src.contains("NYX_AXUM_TEST=1"),
"AxumRoute must print NYX_AXUM_TEST=1 marker, got: {src}",
);
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
@ -2960,6 +3228,19 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::ActixRoute);
assert!(src.contains("NYX_ACTIX_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
fn code_exec_query_routes_through_shell_safe_payload_helper() {
let mut spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
spec.expected_cap = Cap::CODE_EXEC;
spec.payload_slot = PayloadSlot::QueryParam("cmd".to_owned());
let src = generate_main_rs(&spec, RustShape::AxumRoute);
assert!(
src.contains("nyx_route_uri(\"/run\", Some(\"cmd\"), &nyx_route_payload(&payload))")
);
assert!(src.contains("fn nyx_route_payload(payload: &str) -> String"));
}
#[test]
@ -2967,6 +3248,7 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::RocketRoute);
assert!(src.contains("NYX_ROCKET_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]
@ -2974,6 +3256,7 @@ mod tests {
let spec = make_spec_with(EntryKind::HttpRoute, "run", "src/entry.rs");
let src = generate_main_rs(&spec, RustShape::WarpRoute);
assert!(src.contains("NYX_WARP_TEST=1"));
assert!(src.contains("println!(\"__NYX_SINK_HIT__\");"));
}
#[test]

View file

@ -1,44 +1,24 @@
<?php
// Phase 16 — CodeIgniter-style route, benign sanitised payload.
// CodeIgniter-style route, benign sanitised payload.
namespace CodeIgniter\Router {
class RouteCollection
{
}
}
namespace App\Controllers;
namespace {
use CodeIgniter\Controller;
use CodeIgniter\Router\RouteCollection;
class BaseController
function nyx_register_routes(RouteCollection $routes): void
{
$routes->get('run/(:any)', 'App\\Controllers\\UserController::run');
}
class NyxRoutes extends RouteCollection
class UserController extends Controller
{
public function get(string $path, string $callable)
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
[$class, $method] = explode('::', $callable, 2);
$controller = new $class();
return $controller->$method($payload);
};
return $this;
}
}
$routes = new NyxRoutes();
$routes->get('run', 'UserController::run');
class UserController extends BaseController
{
public function run($payload)
public function run(string $payload): string
{
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . escapeshellarg($payload);
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}
}

View file

@ -1,46 +1,24 @@
<?php
// Phase 16 — CodeIgniter-style route, vulnerable.
// `$routes->get('run', 'UserController::run')` references the
// controller method whose body shells out without sanitisation.
// CodeIgniter-style route, vulnerable.
namespace CodeIgniter\Router {
class RouteCollection
{
}
}
namespace App\Controllers;
namespace {
use CodeIgniter\Controller;
use CodeIgniter\Router\RouteCollection;
class BaseController
function nyx_register_routes(RouteCollection $routes): void
{
$routes->get('run/(:any)', 'App\\Controllers\\UserController::run');
}
class NyxRoutes extends RouteCollection
class UserController extends Controller
{
public function get(string $path, string $callable)
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
[$class, $method] = explode('::', $callable, 2);
$controller = new $class();
return $controller->$method($payload);
};
return $this;
}
}
$routes = new NyxRoutes();
$routes->get('run', 'UserController::run');
class UserController extends BaseController
{
public function run($payload)
public function run(string $payload): string
{
echo "__NYX_SINK_HIT__\n";
$cmd = "echo hello " . $payload;
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}
}

View file

@ -1,40 +1,23 @@
<?php
// Phase 16 — Laravel-style route, benign sanitised payload.
// Laravel-style route, benign sanitised payload.
namespace Illuminate\Support\Facades {
class Route
{
public static function get(string $path, string $callable)
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
[$class, $method] = preg_split('/@|::/', $callable);
$controller = new $class();
return $controller->$method($payload);
};
return new class {
public function middleware($value)
{
return $this;
}
};
}
}
namespace App\Http\Controllers;
use Illuminate\Routing\Router;
function nyx_register_routes(Router $router): void
{
$router->get('/run/{payload}', [UserController::class, 'run']);
}
namespace {
use Illuminate\Support\Facades\Route;
Route::get('/run', 'UserController@run');
class UserController
{
public function run($payload)
public function run(string $payload): string
{
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . escapeshellarg($payload);
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}
}

View file

@ -1,42 +1,23 @@
<?php
// Phase 16 — Laravel-style route, vulnerable.
// `Route::get('/run', 'UserController@run')` references the
// controller method whose body shells out without sanitisation.
// Laravel-style route, vulnerable.
namespace Illuminate\Support\Facades {
class Route
{
public static function get(string $path, string $callable)
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($callable) {
[$class, $method] = preg_split('/@|::/', $callable);
$controller = new $class();
return $controller->$method($payload);
};
return new class {
public function middleware($value)
{
return $this;
}
};
}
}
namespace App\Http\Controllers;
use Illuminate\Routing\Router;
function nyx_register_routes(Router $router): void
{
$router->get('/run/{payload}', [UserController::class, 'run']);
}
namespace {
use Illuminate\Support\Facades\Route;
Route::get('/run', 'UserController@run');
class UserController
{
public function run($payload)
public function run(string $payload): string
{
echo "__NYX_SINK_HIT__\n";
$cmd = "echo hello " . $payload;
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}
}

View file

@ -2,53 +2,27 @@
// Same route shape as vuln.php, but quotes the payload before invoking
// the shell so the command-injection marker remains inert.
namespace Illuminate\Support\Facades {
class Route
{
public static function match(array $methods, string $path, array $callable)
{
\NyxLaravelMultiVerb\register_route($methods, $callable);
return new class {
public function middleware($value)
{
return $this;
}
};
}
}
namespace App\Http\Controllers;
use Illuminate\Routing\Router;
function nyx_register_routes(Router $router): void
{
$router->match(['GET', 'POST'], '/run/{payload}', [UserController::class, 'run']);
}
namespace NyxLaravelMultiVerb {
use Illuminate\Support\Facades\Route;
function register_route(array $methods, array $callable): void
class UserController
{
public function run(string $payload): ?string
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (!in_array($requestMethod, $methods, true)) {
return null;
}
[$class, $method] = $callable;
$controller = new $class();
return $controller->$method($payload);
};
}
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
class UserController
{
public function run(string $payload)
{
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
echo "__NYX_METHOD_SKIP__\n";
return null;
}
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . escapeshellarg($payload);
$out = shell_exec($cmd);
echo $out;
return $out;
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
echo "__NYX_METHOD_SKIP__\n";
return null;
}
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . escapeshellarg($payload);
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}

View file

@ -0,0 +1,7 @@
{
"name": "nyx/fixture-laravel-multi-verb",
"require": {
"php": ">=8.1",
"laravel/framework": "^11.0"
}
}

View file

@ -1,55 +1,28 @@
<?php
// Laravel-style multi-verb route fixture. The vulnerable sink is gated
// to POST so verifier runs that exercise only the representative GET
// method miss the command injection.
// to POST so verifier runs that exercise only GET miss the command injection.
namespace Illuminate\Support\Facades {
class Route
{
public static function match(array $methods, string $path, array $callable)
{
\NyxLaravelMultiVerb\register_route($methods, $callable);
return new class {
public function middleware($value)
{
return $this;
}
};
}
}
namespace App\Http\Controllers;
use Illuminate\Routing\Router;
function nyx_register_routes(Router $router): void
{
$router->match(['GET', 'POST'], '/run/{payload}', [UserController::class, 'run']);
}
namespace NyxLaravelMultiVerb {
use Illuminate\Support\Facades\Route;
function register_route(array $methods, array $callable): void
class UserController
{
public function run(string $payload): ?string
{
$GLOBALS['__nyx_route'] = function (string $payload) use ($methods, $callable) {
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if (!in_array($requestMethod, $methods, true)) {
return null;
}
[$class, $method] = $callable;
$controller = new $class();
return $controller->$method($payload);
};
}
Route::match(['GET', 'POST'], '/run', [UserController::class, 'run']);
class UserController
{
public function run(string $payload)
{
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
echo "__NYX_METHOD_SKIP__\n";
return null;
}
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . $payload;
$out = shell_exec($cmd);
echo $out;
return $out;
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
echo "__NYX_METHOD_SKIP__\n";
return null;
}
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . $payload;
$out = shell_exec($cmd) ?? '';
echo $out;
return $out;
}
}

View file

@ -1,48 +1,35 @@
<?php
// Phase 16 — Symfony-style route via `#[Route]` attribute,
// benign sanitised payload.
// Symfony-style route via RouteCollection and HttpKernel, benign sanitised payload.
namespace Symfony\Component\HttpFoundation {
class Response
{
public function __construct(private string $content)
{
}
public function __toString(): string
{
return $this->content;
}
}
}
namespace Symfony\Component\Routing\Annotation {
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
class Route
{
public function __construct(...$args)
{
}
}
}
namespace App\Controller {
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Route as SymfonyRoute;
use Symfony\Component\Routing\RouteCollection;
function nyx_register_routes(RouteCollection $routes): void
{
$routes->add('nyx_run', new SymfonyRoute(
'/run/{payload}',
['_controller' => [new UserController(), 'run']],
[],
[],
'',
[],
['GET']
));
}
class UserController
{
#[Route('/run', methods: ['GET'])]
public function run($payload)
#[Route('/run/{payload}', methods: ['GET'])]
public function run(string $payload): Response
{
echo "__NYX_SINK_HIT__\n";
$cmd = "true " . escapeshellarg($payload);
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return new Response($out);
}
}
$GLOBALS['__nyx_controller'] = new UserController();
}

View file

@ -1,48 +1,35 @@
<?php
// Phase 16 — Symfony-style route via `#[Route]` attribute,
// vulnerable.
// Symfony-style route via RouteCollection and HttpKernel, vulnerable.
namespace Symfony\Component\HttpFoundation {
class Response
{
public function __construct(private string $content)
{
}
public function __toString(): string
{
return $this->content;
}
}
}
namespace Symfony\Component\Routing\Annotation {
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
class Route
{
public function __construct(...$args)
{
}
}
}
namespace App\Controller {
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Route as SymfonyRoute;
use Symfony\Component\Routing\RouteCollection;
function nyx_register_routes(RouteCollection $routes): void
{
$routes->add('nyx_run', new SymfonyRoute(
'/run/{payload}',
['_controller' => [new UserController(), 'run']],
[],
[],
'',
[],
['GET']
));
}
class UserController
{
#[Route('/run', methods: ['GET'])]
public function run($payload)
#[Route('/run/{payload}', methods: ['GET'])]
public function run(string $payload): Response
{
echo "__NYX_SINK_HIT__\n";
$cmd = "echo hello " . $payload;
$out = shell_exec($cmd);
$out = shell_exec($cmd) ?? '';
echo $out;
return new Response($out);
}
}
$GLOBALS['__nyx_controller'] = new UserController();
}

View file

@ -48,14 +48,14 @@ fn laravel_vuln_fixture_binds_route() {
assert_eq!(binding.adapter, "php-laravel");
assert_eq!(binding.kind, EntryKind::HttpRoute);
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.path, "/run/{payload}");
assert_eq!(route.method, HttpMethod::GET);
let payload = binding
.request_params
.iter()
.find(|p| p.name == "payload")
.expect("payload formal");
assert!(matches!(payload.source, ParamSource::QueryParam(_)));
assert!(matches!(payload.source, ParamSource::PathSegment(_)));
}
#[test]
@ -68,7 +68,7 @@ fn laravel_benign_fixture_binds_same_route_shape() {
.expect("laravel adapter must bind benign fixture");
assert_eq!(binding.adapter, "php-laravel");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.path, "/run/{payload}");
assert_eq!(route.method, HttpMethod::GET);
}
@ -82,7 +82,7 @@ fn laravel_multi_verb_fixture_preserves_match_methods() {
.expect("laravel adapter must bind multi-verb fixture");
assert_eq!(binding.adapter, "php-laravel");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.path, "/run/{payload}");
assert_eq!(route.method, HttpMethod::GET);
assert_eq!(
route.reachable_methods(),
@ -101,7 +101,7 @@ fn symfony_vuln_fixture_binds_route_via_attribute() {
assert_eq!(binding.adapter, "php-symfony");
assert_eq!(binding.kind, EntryKind::HttpRoute);
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.path, "/run/{payload}");
assert_eq!(route.method, HttpMethod::GET);
}
@ -115,7 +115,7 @@ fn symfony_benign_fixture_binds_same_route_shape() {
.expect("symfony adapter must bind benign fixture");
assert_eq!(binding.adapter, "php-symfony");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "/run");
assert_eq!(route.path, "/run/{payload}");
}
#[test]
@ -153,7 +153,7 @@ fn codeigniter_vuln_fixture_binds_route() {
assert_eq!(binding.adapter, "php-codeigniter");
assert_eq!(binding.kind, EntryKind::HttpRoute);
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "run");
assert_eq!(route.path, "run/(:any)");
assert_eq!(route.method, HttpMethod::GET);
}
@ -167,7 +167,7 @@ fn codeigniter_benign_fixture_binds_same_route_shape() {
.expect("codeigniter adapter must bind benign fixture");
assert_eq!(binding.adapter, "php-codeigniter");
let route = binding.route.as_ref().expect("route");
assert_eq!(route.path, "run");
assert_eq!(route.path, "run/(:any)");
}
#[test]
@ -270,6 +270,13 @@ mod e2e_phase_16_framework_dispatchers {
let tmp = TempDir::new().expect("create tempdir");
let dst = tmp.path().join(file);
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
for manifest in ["composer.json", "composer.lock"] {
let candidate = src.parent().expect("fixture parent").join(manifest);
if candidate.exists() {
std::fs::copy(&candidate, tmp.path().join(manifest))
.expect("copy composer manifest into tempdir");
}
}
let entry_file = dst.to_string_lossy().into_owned();
let bytes = std::fs::read(&dst).expect("copied fixture readable");
let tree = parse_php(&bytes);
@ -425,6 +432,13 @@ mod e2e_phase_16_laravel_multi_verb {
let tmp = TempDir::new().expect("create tempdir");
let dst = tmp.path().join(file);
std::fs::copy(&src, &dst).expect("copy fixture into tempdir");
for manifest in ["composer.json", "composer.lock"] {
let candidate = src.parent().expect("fixture parent").join(manifest);
if candidate.exists() {
std::fs::copy(&candidate, tmp.path().join(manifest))
.expect("copy composer manifest into tempdir");
}
}
let entry_file = dst.to_string_lossy().into_owned();
let bytes = std::fs::read(&dst).expect("copied fixture readable");
let tree = parse_php(&bytes);