use std::path::Path;
use oxc_ast::ast::*;
use oxc_ast_visit::Visit;
use oxc_span::GetSpan;
use crate::rules::{Issue, IssueCategory, IssueSource, Rule, RuleContext, Severity};
use crate::utils::offset_to_line_col;
pub struct NoXssViaJsxProp;
impl Rule for NoXssViaJsxProp {
fn name(&self) -> &str {
"no_xss_via_jsx_prop"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = Visitor {
source_text: ctx.source_text,
file_path: ctx.file_path,
issues: vec![],
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct Visitor<'a> {
source_text: &'a str,
file_path: &'a Path,
issues: Vec<Issue>,
}
const TAINTED_ROOTS: &[&str] = &["req", "request", "ctx"];
const TAINTED_PROPS: &[&str] = &[
"query",
"body",
"params",
"headers",
"cookies",
"searchParams",
];
impl<'a, 'b> Visit<'b> for Visitor<'a> {
fn visit_jsx_attribute(&mut self, attr: &JSXAttribute<'b>) {
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value else {
return;
};
let Some(expr) = container.expression.as_expression() else {
return;
};
if let Some((root, prop)) = extract_member_root(expr) {
if TAINTED_ROOTS.contains(&root) && TAINTED_PROPS.contains(&prop) {
let prop_name = match &attr.name {
JSXAttributeName::Identifier(id) => id.name.as_str().to_string(),
JSXAttributeName::NamespacedName(nn) => nn.name.name.as_str().to_string(),
};
let (line, col) = offset_to_line_col(self.source_text, expr.span().start);
self.issues.push(Issue {
rule: "no_xss_via_jsx_prop".into(),
message: format!(
"JSX prop `{prop_name}` receives `{root}.{prop}` directly — \
user-controlled data in SSR can cause reflected XSS"
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::High,
source: IssueSource::ReactPerfAnalyzer,
category: IssueCategory::Security,
});
}
}
}
}
fn extract_member_root<'b>(expr: &Expression<'b>) -> Option<(&'b str, &'b str)> {
match expr {
Expression::StaticMemberExpression(mem) => {
let prop_name = mem.property.name.as_str();
match &mem.object {
Expression::Identifier(id) => Some((id.name.as_str(), prop_name)),
Expression::StaticMemberExpression(inner) => {
let inner_prop = inner.property.name.as_str();
if let Expression::Identifier(root_id) = &inner.object {
Some((root_id.name.as_str(), inner_prop))
} else {
None
}
}
_ => None,
}
}
_ => None,
}
}