perl_lsp_diagnostics/lints/
security.rs1use perl_diagnostics_codes::DiagnosticCode;
16use perl_parser_core::ast::{Node, NodeKind};
17
18use super::super::walker::walk_node;
19use perl_lsp_diagnostic_types::{Diagnostic, DiagnosticSeverity, RelatedInformation};
20
21pub fn check_security(node: &Node, diagnostics: &mut Vec<Diagnostic>) {
28 walk_node(node, &mut |n| {
29 match &n.kind {
30 NodeKind::FunctionCall { name, args } => {
31 check_two_arg_open(name, args, n, diagnostics);
32 check_string_eval(name, args, n, diagnostics);
33 }
34
35 NodeKind::Eval { block } => {
39 check_eval_node(block, n, diagnostics);
40 }
41
42 NodeKind::String { value, interpolated: true } if is_backtick_string(value) => {
45 diagnostics.push(Diagnostic {
46 range: (n.location.start, n.location.end),
47 severity: DiagnosticSeverity::Information,
48 code: Some(DiagnosticCode::SecurityBacktickExec.as_str().to_string()),
49 message: "Command execution detected. Ensure input is sanitized.".to_string(),
50 related_information: vec![RelatedInformation {
51 location: (n.location.start, n.location.end),
52 message: "Consider using open() with a pipe, or IPC::Run for safer command execution with proper input validation".to_string(),
53 }],
54 tags: Vec::new(),
55 suggestion: Some(
56 "Use open(my $fh, '-|', @cmd) or IPC::Run for safer command execution"
57 .to_string(),
58 ),
59 });
60 }
61
62 _ => {}
63 }
64 });
65}
66
67fn check_eval_node(block: &Node, eval_node: &Node, diagnostics: &mut Vec<Diagnostic>) {
73 let is_string_eval = matches!(&block.kind, NodeKind::String { .. } | NodeKind::Variable { .. })
74 || matches!(&block.kind, NodeKind::Binary { op, .. } if op == ".");
75
76 if !is_string_eval {
77 return;
78 }
79
80 diagnostics.push(Diagnostic {
81 range: (eval_node.location.start, eval_node.location.end),
82 severity: DiagnosticSeverity::Warning,
83 code: Some(DiagnosticCode::SecurityStringEval.as_str().to_string()),
84 message: "String eval is a security risk. Consider eval { } for exception handling."
85 .to_string(),
86 related_information: vec![RelatedInformation {
87 location: (eval_node.location.start, eval_node.location.end),
88 message: "String eval executes arbitrary Perl code at runtime. If the string contains user input, this allows code injection.".to_string(),
89 }],
90 tags: Vec::new(),
91 suggestion: Some(
92 "Use eval { } for exception handling, or consider safer alternatives like Try::Tiny"
93 .to_string(),
94 ),
95 });
96}
97
98fn check_two_arg_open(name: &str, args: &[Node], node: &Node, diagnostics: &mut Vec<Diagnostic>) {
103 if name != "open" || args.len() != 2 {
104 return;
105 }
106
107 diagnostics.push(Diagnostic {
108 range: (node.location.start, node.location.end),
109 severity: DiagnosticSeverity::Warning,
110 code: Some(DiagnosticCode::TwoArgOpen.as_str().to_string()),
111 message: "Use 3-argument open for safety: open(my $fh, '>', 'file')".to_string(),
112 related_information: vec![RelatedInformation {
113 location: (node.location.start, node.location.end),
114 message: "Two-argument open combines mode and filename, which can allow shell injection if the filename is derived from user input".to_string(),
115 }],
116 tags: Vec::new(),
117 suggestion: Some("Replace with 3-arg form: open(my $fh, '>', $file)".to_string()),
118 });
119}
120
121fn check_string_eval(name: &str, args: &[Node], node: &Node, diagnostics: &mut Vec<Diagnostic>) {
127 if name != "eval" {
128 return;
129 }
130
131 let is_string_arg = args.first().is_some_and(|arg| match &arg.kind {
136 NodeKind::String { .. } | NodeKind::Variable { .. } => true,
137 NodeKind::Binary { op, .. } if op == "." => true,
138 _ => false,
139 });
140
141 if !is_string_arg && !args.is_empty() {
142 return;
143 }
144
145 diagnostics.push(Diagnostic {
146 range: (node.location.start, node.location.end),
147 severity: DiagnosticSeverity::Warning,
148 code: Some(DiagnosticCode::SecurityStringEval.as_str().to_string()),
149 message: "String eval is a security risk. Consider eval { } for exception handling."
150 .to_string(),
151 related_information: vec![RelatedInformation {
152 location: (node.location.start, node.location.end),
153 message: "String eval executes arbitrary Perl code at runtime. If the string contains user input, this allows code injection.".to_string(),
154 }],
155 tags: Vec::new(),
156 suggestion: Some(
157 "Use eval { } for exception handling, or consider safer alternatives like Try::Tiny"
158 .to_string(),
159 ),
160 });
161}
162
163fn is_backtick_string(value: &str) -> bool {
168 value.starts_with('`') && value.ends_with('`') && value.len() >= 2
169}