1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! vue/no-unsafe-url
//!
//! Warn about potentially unsafe URL bindings.
//!
//! Dynamic URLs in href and src attributes can be exploited for XSS
//! attacks using `javascript:` protocol or data URLs.
//!
//! ## Security Risks
//!
//! - JavaScript execution via `javascript:` protocol
//! - Data exfiltration via malicious URLs
//! - Phishing through open redirects
//!
//! ## Examples
//!
//! ### Requires Attention
//! ```vue
//! <!-- User-provided URLs need sanitization -->
//! <a :href="userProvidedUrl">Link</a>
//! <iframe :src="dynamicUrl"></iframe>
//! <img :src="imageUrl" />
//! ```
//!
//! ### Safe Patterns
//! ```vue
//! <!-- Static URLs are safe -->
//! <a href="/about">About</a>
//!
//! <!-- Computed URLs with validation -->
//! <a :href="sanitizedUrl">Link</a>
//!
//! <!-- Using router-link instead of href -->
//! <router-link :to="{ name: 'profile', params: { id } }">Profile</router-link>
//! ```
//!
//! ## Best Practices
//!
//! 1. Sanitize URLs on the backend before storing
//! 2. Use `@braintree/sanitize-url` for frontend validation
//! 3. Prefer `<router-link>` over `<a :href="">`
use crate::context::LintContext;
use crate::diagnostic::Severity;
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::{DirectiveNode, ElementNode, ExpressionNode};
static META: RuleMeta = RuleMeta {
name: "vue/no-unsafe-url",
description: "Warn about potentially unsafe URL bindings",
category: RuleCategory::Recommended,
fixable: false,
default_severity: Severity::Warning,
};
/// No unsafe URL binding rule
#[derive(Default)]
pub struct NoUnsafeUrl;
/// Attributes that can be exploited with unsafe URLs
const UNSAFE_URL_ATTRS: &[&str] = &["href", "src", "srcset", "action", "formaction"];
impl Rule for NoUnsafeUrl {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn check_directive<'a>(
&self,
ctx: &mut LintContext<'a>,
element: &ElementNode<'a>,
directive: &DirectiveNode<'a>,
) {
// Only check v-bind
if directive.name != "bind" {
return;
}
// Get the attribute name
let attr_name = match &directive.arg {
Some(ExpressionNode::Simple(s)) => s.content.as_str(),
_ => return,
};
// Check if this is a potentially unsafe attribute
if !UNSAFE_URL_ATTRS.contains(&attr_name) {
return;
}
// Skip if the element is router-link (it handles routing safely)
let tag = element.tag.as_str();
if tag == "router-link" || tag == "RouterLink" || tag == "nuxt-link" || tag == "NuxtLink" {
return;
}
let help_message = if attr_name == "href" {
ctx.t("vue/no-unsafe-url.help_href")
} else {
ctx.t("vue/no-unsafe-url.help")
};
ctx.warn_with_help(
ctx.t("vue/no-unsafe-url.message"),
&directive.loc,
help_message,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::Linter;
use crate::rule::RuleRegistry;
fn create_linter() -> Linter {
let mut registry = RuleRegistry::new();
registry.register(Box::new(NoUnsafeUrl));
Linter::with_registry(registry)
}
#[test]
fn test_valid_static_href() {
let linter = create_linter();
let result = linter.lint_template(r#"<a href="/about">About</a>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_router_link() {
let linter = create_linter();
let result = linter.lint_template(
r#"<router-link :to="{ name: 'profile' }">Profile</router-link>"#,
"test.vue",
);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_warns_dynamic_href() {
let linter = create_linter();
let result = linter.lint_template(r#"<a :href="userUrl">Link</a>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_warns_dynamic_src() {
let linter = create_linter();
let result = linter.lint_template(r#"<iframe :src="url"></iframe>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_valid_class_binding() {
let linter = create_linter();
let result = linter.lint_template(r#"<div :class="classes"></div>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
}