1const SENSITIVE_FIELD_PATTERNS: &[&str] = &[
8 "password",
9 "token",
10 "secret",
11 "key",
12 "credential",
13 "auth",
14 "private",
15 "cert",
16];
17
18const MAX_INPUT_LENGTH: usize = 10_000;
19
20fn is_sensitive_field_name(field_name: &str) -> bool {
22 let lower = field_name.to_lowercase();
23 SENSITIVE_FIELD_PATTERNS
24 .iter()
25 .any(|pattern| lower.contains(pattern))
26}
27
28fn is_sensitive_value(value: &str) -> bool {
30 if value.len() >= 8 {
32 let has_uppercase = value.chars().any(|c| c.is_uppercase());
34 let has_lowercase = value.chars().any(|c| c.is_lowercase());
35 let has_digit = value.chars().any(|c| c.is_ascii_digit());
36 let has_special = value.chars().any(|c| !c.is_alphanumeric());
37
38 if has_uppercase && has_lowercase && has_digit && has_special {
39 return true;
40 }
41 }
42 false
43}
44
45fn emit_security_warning(field_name: &str, _value: &str) {
47 eprintln!(
49 "⚠️ SECURITY WARNING: Hardcoded sensitive value detected for field '{}'.\n\
50 Consider using environment variables instead to avoid embedding secrets in compiled code.\n\
51 Example: Use #[config({}_env = \"MY_SECRET_VAR\")] instead of #[config({} = \"...\")]",
52 field_name,
53 field_name.split("_").next().unwrap_or(field_name),
54 field_name
55 );
56}
57
58use darling::FromDeriveInput;
59use proc_macro::TokenStream;
60use proc_macro2::TokenStream as ProcMacro2TokenStream;
61use syn::{parse_macro_input, DeriveInput, Meta};
62
63mod codegen;
64mod confers_common;
65mod parse;
66
67fn has_serde_flatten(attrs: &Vec<syn::Attribute>) -> bool {
68 for attr in attrs {
69 if attr.path().is_ident("serde") {
70 if let syn::Meta::List(list) = &attr.meta {
71 let s = list.tokens.to_string();
72 if s.contains("flatten") {
73 return true;
74 }
75 }
76 }
77 }
78 false
79}
80
81fn unescape_rust_string(content: &str) -> String {
84 let mut result = String::with_capacity(content.len());
85 let mut chars = content.chars().peekable();
86
87 while let Some(c) = chars.next() {
88 if c == '\\' {
89 match chars.peek() {
90 Some(&'\\') => {
91 result.push('\\');
92 chars.next();
93 }
94 Some(&'"') => {
95 result.push('"');
96 chars.next();
97 }
98 Some(&'n') => {
99 result.push('\n');
100 chars.next();
101 }
102 Some(&'t') => {
103 result.push('\t');
104 chars.next();
105 }
106 Some(&'0') => {
107 result.push('\0');
108 chars.next();
109 }
110 _ => {
111 result.push(c);
112 }
113 }
114 } else {
115 result.push(c);
116 }
117 }
118 result
119}
120
121fn has_validate_derive(input: &syn::DeriveInput) -> bool {
122 for attr in &input.attrs {
123 if attr.path().is_ident("derive") {
124 if let syn::Meta::List(list) = &attr.meta {
125 let tokens_str = list.tokens.to_string();
126 if tokens_str.contains("Validate") {
127 return true;
128 }
129 }
130 }
131 }
132 false
133}
134
135fn extract_default_value(tokens_str: &str) -> Option<(String, bool, bool)> {
137 if tokens_str.len() > MAX_INPUT_LENGTH {
139 eprintln!(
140 "⚠️ SECURITY WARNING: Input token length ({}) exceeds maximum allowed ({}). \
141 Potential denial of service attack detected.",
142 tokens_str.len(),
143 MAX_INPUT_LENGTH
144 );
145 return None;
146 }
147
148 if let Some(start) = tokens_str.find("default = ") {
149 let after_equals = &tokens_str[start + 10..];
150
151 if after_equals.starts_with('"') {
152 let after_first_quote = after_equals
153 .get(1..)
154 .expect("String should have at least one character after quote");
155 let mut i = 0;
156 let mut end_pos = None;
157
158 while i < after_first_quote.len() {
159 let c = after_first_quote.chars().nth(i).unwrap();
160 if c == '\\' && i + 1 < after_first_quote.len() {
161 let next_char = after_first_quote.chars().nth(i + 1).unwrap();
162 if next_char == '"' {
163 i += 2;
164 continue;
165 }
166 }
167 if c == '"' {
168 end_pos = Some(i);
169 break;
170 }
171 i += 1;
172 }
173
174 if let Some(end) = end_pos {
175 let inner_value = &after_first_quote[..end];
176 let already_wrapped = inner_value.contains(".to_string()");
178
179 let (value, wrapped) = if already_wrapped {
180 let before_to_string = inner_value
183 .strip_suffix(".to_string()")
184 .unwrap_or(inner_value);
185
186 let content = if before_to_string.starts_with("\\\"")
193 && before_to_string.ends_with("\\\"")
194 {
195 &before_to_string[2..before_to_string.len() - 2]
196 } else if before_to_string.starts_with('"') && before_to_string.ends_with('"') {
197 before_to_string
199 .strip_prefix('"')
200 .and_then(|s| s.strip_suffix('"'))
201 .unwrap_or(before_to_string)
202 } else {
203 before_to_string
204 };
205
206 let unescaped = unescape_rust_string(content);
208
209 (unescaped, true)
210 } else {
211 let parse_str = format!("\"{}\"", inner_value);
214 if let Ok(lit_str) = syn::parse_str::<syn::LitStr>(&parse_str) {
215 (lit_str.value(), false)
216 } else {
217 let content = inner_value
219 .strip_prefix('"')
220 .and_then(|s| s.strip_suffix('"'))
221 .unwrap_or(inner_value);
222 let unescaped = unescape_rust_string(content);
223 (unescaped, false)
224 }
225 };
226 return Some((value, wrapped, true));
227 }
228 } else {
229 let value = after_equals.trim();
230 let already_wrapped = value.contains(".to_string()");
231 return Some((value.to_string(), already_wrapped, false));
232 }
233 }
234 None
235}
236
237fn process_meta_name_value(nv: &syn::MetaNameValue, opts: &mut parse::FieldOpts) {
238 let ident = nv.path.get_ident().map(|i| i.to_string());
239 let field_name = opts
240 .ident
241 .as_ref()
242 .map(|i| i.to_string())
243 .unwrap_or_default();
244 let is_sensitive_field = is_sensitive_field_name(&field_name);
245
246 match ident.as_deref() {
247 Some("description") => {
248 if let syn::Expr::Lit(syn::ExprLit {
249 lit: syn::Lit::Str(s),
250 ..
251 }) = &nv.value
252 {
253 opts.description = Some(s.value());
254 }
255 }
256 Some("default") => {
257 if opts.default.is_none() {
258 opts.default = Some(nv.value.clone());
259 }
260 }
261 Some("name") => {
262 if let syn::Expr::Lit(syn::ExprLit {
263 lit: syn::Lit::Str(s),
264 ..
265 }) = &nv.value
266 {
267 opts.name_config = Some(s.value());
268 }
269 }
270 Some("name_env") => {
271 if let syn::Expr::Lit(syn::ExprLit {
272 lit: syn::Lit::Str(s),
273 ..
274 }) = &nv.value
275 {
276 opts.name_env = Some(s.value());
277 }
278 }
279 Some("validate") => {
280 if let syn::Expr::Lit(syn::ExprLit {
281 lit: syn::Lit::Str(s),
282 ..
283 }) = &nv.value
284 {
285 opts.validate = Some(s.value());
286 } else if let syn::Expr::Lit(syn::ExprLit {
287 lit: syn::Lit::Bool(b),
288 ..
289 }) = &nv.value
290 {
291 if b.value {
292 opts.validate = Some("true".to_string());
293 }
294 } else {
295 let s = quote::quote!(#nv.value).to_string();
296 opts.validate = Some(s);
297 }
298 }
299 Some("custom_validate") => {
300 if let syn::Expr::Lit(syn::ExprLit {
301 lit: syn::Lit::Str(s),
302 ..
303 }) = &nv.value
304 {
305 opts.custom_validate = Some(s.value());
306 }
307 }
308 Some("sensitive") => {
309 if let syn::Expr::Lit(syn::ExprLit {
310 lit: syn::Lit::Bool(b),
311 ..
312 }) = &nv.value
313 {
314 opts.sensitive = Some(b.value);
315 }
316 }
317 Some("remote") => {
318 if let syn::Expr::Lit(syn::ExprLit {
319 lit: syn::Lit::Str(s),
320 ..
321 }) = &nv.value
322 {
323 opts.remote = Some(s.value());
324 }
325 }
326 Some("remote_timeout") => {
327 if let syn::Expr::Lit(syn::ExprLit {
328 lit: syn::Lit::Str(s),
329 ..
330 }) = &nv.value
331 {
332 opts.remote_timeout = Some(s.value());
333 }
334 }
335 Some("remote_auth") => {
336 if let syn::Expr::Lit(syn::ExprLit {
337 lit: syn::Lit::Bool(b),
338 ..
339 }) = &nv.value
340 {
341 opts.remote_auth = Some(b.value);
342 }
343 }
344 Some("remote_username") => {
345 if let syn::Expr::Lit(syn::ExprLit {
346 lit: syn::Lit::Str(s),
347 ..
348 }) = &nv.value
349 {
350 opts.remote_username = Some(s.value());
351 }
352 }
353 Some("remote_password") => {
354 if let syn::Expr::Lit(syn::ExprLit {
355 lit: syn::Lit::Str(s),
356 ..
357 }) = &nv.value
358 {
359 let value = s.value();
360 if is_sensitive_field || is_sensitive_value(&value) {
362 emit_security_warning("remote_password", &value);
363 }
364 opts.remote_password = Some(value);
365 }
366 }
367 Some("remote_token") => {
368 if let syn::Expr::Lit(syn::ExprLit {
369 lit: syn::Lit::Str(s),
370 ..
371 }) = &nv.value
372 {
373 let value = s.value();
374 if is_sensitive_field || is_sensitive_value(&value) {
376 emit_security_warning("remote_token", &value);
377 }
378 opts.remote_token = Some(value);
379 }
380 }
381 Some("remote_tls") => {
382 if let syn::Expr::Lit(syn::ExprLit {
383 lit: syn::Lit::Bool(b),
384 ..
385 }) = &nv.value
386 {
387 opts.remote_tls = Some(b.value);
388 }
389 }
390 Some("remote_ca_cert") => {
391 if let syn::Expr::Lit(syn::ExprLit {
392 lit: syn::Lit::Str(s),
393 ..
394 }) = &nv.value
395 {
396 opts.remote_ca_cert = Some(s.value());
397 }
398 }
399 Some("remote_client_cert") => {
400 if let syn::Expr::Lit(syn::ExprLit {
401 lit: syn::Lit::Str(s),
402 ..
403 }) = &nv.value
404 {
405 opts.remote_client_cert = Some(s.value());
406 }
407 }
408 Some("remote_client_key") => {
409 if let syn::Expr::Lit(syn::ExprLit {
410 lit: syn::Lit::Str(s),
411 ..
412 }) = &nv.value
413 {
414 let value = s.value();
415 if is_sensitive_field || is_sensitive_value(&value) {
417 emit_security_warning("remote_client_key", &value);
418 }
419 opts.remote_client_key = Some(value);
420 }
421 }
422 _ => {}
423 }
424}
425
426fn parse_field_opts(field: &syn::Field) -> parse::FieldOpts {
427 let mut opts = parse::FieldOpts {
428 ident: field.ident.clone(),
429 ty: field.ty.clone(),
430 attrs: field.attrs.clone(),
431 description: None,
432 default: None,
433 flatten: false,
434 serde_flatten: false,
435 skip: false,
436 name_config: None,
437 name_env: None,
438 name_clap_long: None,
439 name_clap_short: None,
440 validate: None,
441 custom_validate: None,
442 sensitive: None,
443 remote: None,
444 remote_timeout: None,
445 remote_auth: None,
446 remote_username: None,
447 remote_password: None,
448 remote_token: None,
449 remote_tls: None,
450 remote_ca_cert: None,
451 remote_client_cert: None,
452 remote_client_key: None,
453 };
454
455 for attr in &field.attrs {
456 if !attr.path().is_ident("config") {
457 continue;
458 }
459
460 if let Meta::List(list) = &attr.meta {
461 let tokens_str = list.tokens.to_string();
462
463 if let Some((value, already_wrapped, is_string)) = extract_default_value(&tokens_str) {
464 if is_string {
465 if already_wrapped {
466 let lit = syn::LitStr::new(&value, proc_macro2::Span::call_site());
467 let expr: syn::Expr = syn::parse_quote! { #lit };
468 opts.default = Some(expr);
469 } else {
470 let wrapped_str = format!("\"{}\".to_string()", value);
471 if let Ok(expr) = syn::parse_str::<syn::Expr>(&wrapped_str) {
472 opts.default = Some(expr);
473 }
474 }
475 }
476
477 if !is_string {
478 if already_wrapped {
479 let wrapped_str = format!("{}.to_string()", value);
480 if let Ok(expr) = syn::parse_str::<syn::Expr>(&wrapped_str) {
481 opts.default = Some(expr);
482 }
483 }
484
485 if !already_wrapped {
486 if let Ok(expr) = syn::parse_str::<syn::Expr>(&value) {
487 opts.default = Some(expr);
488 }
489 }
490 }
491 }
492
493 let tokens_stream: ProcMacro2TokenStream = list.tokens.clone().into_iter().collect();
495
496 if let Ok(nv) = syn::parse2::<syn::MetaNameValue>(tokens_stream.clone()) {
498 process_meta_name_value(&nv, &mut opts);
499 } else {
500 let mut current_nv_tokens = ProcMacro2TokenStream::new();
502 let mut expect_value = false;
503
504 for token in tokens_stream {
505 if let proc_macro2::TokenTree::Ident(ident) = &token {
506 let ident_str = ident.to_string();
507 if ident_str == "flatten" {
509 opts.flatten = true;
510 continue;
511 } else if ident_str == "skip" {
512 opts.skip = true;
513 continue;
514 }
515 }
516
517 if let proc_macro2::TokenTree::Punct(punct) = &token {
518 if punct.as_char() == '=' && !expect_value {
519 expect_value = true;
520 continue;
521 }
522 }
523
524 if expect_value {
525 current_nv_tokens = ProcMacro2TokenStream::new();
526 expect_value = false;
527 }
528
529 current_nv_tokens.extend(std::iter::once(token));
530
531 if current_nv_tokens.clone().into_iter().next().is_some() {
533 let has_equals = current_nv_tokens.clone().into_iter().any(|t| {
535 if let proc_macro2::TokenTree::Punct(p) = t {
536 p.as_char() == '='
537 } else {
538 false
539 }
540 });
541
542 if has_equals {
543 if let Ok(nv) =
544 syn::parse2::<syn::MetaNameValue>(current_nv_tokens.clone())
545 {
546 process_meta_name_value(&nv, &mut opts);
547 current_nv_tokens = ProcMacro2TokenStream::new();
548 }
549 }
550 }
551 }
552
553 for item in list.tokens.clone().into_iter() {
555 if let proc_macro2::TokenTree::Group(group) = item {
556 for inner in group.stream().into_iter() {
557 if let Ok(nv) = syn::parse2::<syn::MetaNameValue>(
558 ProcMacro2TokenStream::from(inner),
559 ) {
560 process_meta_name_value(&nv, &mut opts);
561 }
562 }
563 }
564 }
565 }
566 }
567 }
568
569 opts
570}
571
572#[proc_macro_derive(Config, attributes(config))]
573pub fn derive_config(input: TokenStream) -> TokenStream {
574 let input = parse_macro_input!(input as DeriveInput);
575
576 let opts = match parse::ConfigOpts::from_derive_input(&input) {
577 Ok(val) => val,
578 Err(err) => return err.write_errors().into(),
579 };
580
581 let fields = match &input.data {
582 syn::Data::Struct(data) => {
583 let mut field_opts = Vec::new();
584 for field in &data.fields {
585 let mut f = parse_field_opts(field);
586
587 if has_serde_flatten(&field.attrs) {
588 f.serde_flatten = true;
589 f.flatten = true;
590 }
591
592 field_opts.push(f);
593 }
594 field_opts
595 }
596 _ => {
597 return syn::Error::new_spanned(input, "Config can only be derived for structs")
598 .to_compile_error()
599 .into();
600 }
601 };
602
603 let has_validate = has_validate_derive(&input);
604 codegen::generate_impl(&opts, &fields, has_validate).into()
605}