3dd6582d Andy McCall 21 days ago ↶
1
2
3
4
5
6
7
use super :: settings:: csrf;
8
use super :: { Error, Page, Shell} ;
9
use crate :: auth:: Viewer;
10
use crate :: db:: { self , accounts} ;
11
use crate :: Shared;
12
use axum:: extract:: { Form, State} ;
13
use axum:: http:: { header, StatusCode} ;
14
use axum:: response:: { IntoResponse, Response} ;
15
use serde:: Deserialize;
16
17
const CONTACT : & str = " security_contact" ;
18
const POLICY : & str = " security_policy" ;
2cbf6f34 Andy McCall 20 days ago ↶
19
const EMAIL : & str = " security_email" ;
20
const PAGE : & str = " security_page" ;
21
22
const MAX_PAGE : usize = 20_000 ;
23
24
25
26
pub const DEFAULT_PAGE : & str = " If you think you have found a security problem in this hub, please tell us privately rather than in a public issue, proposal or comment.\n \n \
27
Please include:\n \n \
28
- what you found, and where (the page, the API call, the repository if one is involved);\n \
29
- how to reproduce it, step by step;\n \
30
- what you think someone could do with it.\n \n \
31
While you look, please do not read or change other people's data beyond what you need to show the problem, do not run tests that slow the service down for others, and give us a fair chance to fix it before you tell anyone else.\n " ;
3dd6582d Andy McCall 21 days ago ↶
32
33
const EXPIRES_MS : i64 = 360 * 24 * 60 * 60_000 ;
2cbf6f34 Andy McCall 20 days ago ↶
34
35
36
pub async fn page_settings ( app : & Shared) -> ( String, String) {
37
let get = | k | async move { accounts:: setting( & app.db, k) .await.ok ( ) .flatten ( ) .unwrap_or_default ( ) } ;
38
( get ( EMAIL ) .await, get ( PAGE ) .await)
39
}
40
41
# [ derive ( askama::Template ) ]
42
# [ template ( path = " security_contact.html" ) ]
43
pub struct ContactPage {
44
shell : Shell,
45
email : String,
46
47
body_html : String,
48
}
3dd6582d Andy McCall 21 days ago ↶
49
2cbf6f34 Andy McCall 20 days ago ↶
50
51
pub async fn contact_page ( State( app ) : State< Shared> , viewer : Viewer) -> Page< ContactPage> {
52
let shell = Shell:: new( & app, " /security-contact" , & viewer) ;
53
let ( email, text) = page_settings ( & app) .await;
54
let text = if text.trim ( ) .is_empty ( ) { DEFAULT_PAGE .to_string ( ) } else { text } ;
55
Page( ContactPage { shell, email, body_html: crate :: render:: markdown:: render( & text, " " , " " ) } )
56
}
57
3dd6582d Andy McCall 21 days ago ↶
58
59
pub async fn settings ( app : & Shared) -> ( String, String) {
60
let get = | k | async move { accounts:: setting( & app.db, k) .await.ok ( ) .flatten ( ) .unwrap_or_default ( ) } ;
61
( get ( CONTACT ) .await, get ( POLICY ) .await)
62
}
63
64
65
66
pub fn contact_uri ( contact : & str ) -> Option< String> {
67
let c = contact.trim ( ) ;
68
if c.is_empty ( ) | | c.chars ( ) .any ( | ch | ch.is_whitespace ( ) | | ch.is_control ( ) ) {
69
return None ;
70
}
71
if c.starts_with ( " https://" ) | | c.starts_with ( " mailto:" ) {
72
return Some ( c.to_string ( ) ) ;
73
}
74
let ( user, domain) = c.split_once ( ' @' ) ? ;
75
( ! user.is_empty ( ) & & domain.contains ( ' .' ) & & ! domain.contains ( ' @' ) & & ! c.contains ( ' :' ) ) .then ( | | format! ( " mailto:{c} " ) )
76
}
77
78
79
pub fn text ( base_url : & str , contact : & str , policy : & str , now_ms : i64 ) -> Option< String> {
80
let contact = contact_uri ( contact) ? ;
81
let expires = crate :: render:: time:: rfc3339( ( now_ms + EXPIRES_MS ) as u64 ) ;
82
let base = base_url.trim_end_matches ( ' /' ) ;
83
let mut out = format! ( " Contact: {contact} \n Expires: {expires} \n " ) ;
84
if ! policy.trim ( ) .is_empty ( ) {
85
out.push_str ( & format! ( " Policy: {} \n " , policy.trim ( ) ) ) ;
86
}
87
out.push_str ( & format! ( " Preferred-Languages: en\n Canonical: {base} /.well-known/security.txt\n " ) ) ;
88
Some ( out)
89
}
90
91
92
pub async fn serve ( State( app ) : State< Shared> ) -> Response {
93
let ( contact, policy) = settings ( & app) .await;
94
match text ( app.config.base_url ( ) , & contact, & policy, db:: now_ms( ) ) {
95
Some ( t) => ( [ ( header:: CONTENT_TYPE , " text/plain; charset=utf-8" ) ] , t) .into_response ( ) ,
96
None => ( StatusCode:: NOT_FOUND , " This hub has no security contact set.\n " ) .into_response ( ) ,
97
}
98
}
99
100
# [ derive ( Deserialize ) ]
101
pub struct SecurityForm {
102
#[serde(rename = "_csrf")]
103
csrf_token : String,
104
#[serde(default)]
105
contact : String,
106
#[serde(default)]
107
policy : String,
2cbf6f34 Andy McCall 20 days ago ↶
108
109
#[serde(default)]
110
email : String,
111
112
#[serde(default)]
113
page : String,
3dd6582d Andy McCall 21 days ago ↶
114
}
115
116
117
118
pub async fn save ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< SecurityForm> ) -> Result< Page< super:: admin:: AdminPage> , Error> {
119
let shell = Shell:: new( & app, " /admin" , & viewer) ;
120
let me = super :: admin:: require_admin( & viewer, & shell) ? ;
121
csrf ( & viewer, & f.csrf_token, & shell) ? ;
122
let ( contact, policy) = ( f.contact.trim ( ) , f.policy.trim ( ) ) ;
123
if ! contact.is_empty ( ) & & contact_uri ( contact) .is_none ( ) {
124
return Err ( Error:: bad( shell, " The contact is an email address or an https:// page." ) ) ;
125
}
126
if ! policy.is_empty ( ) & & ( ! policy.starts_with ( " https://" ) | | policy.chars ( ) .any ( char :: is_whitespace) ) {
127
return Err ( Error:: bad( shell, " The policy is an https:// page, or empty." ) ) ;
2cbf6f34 Andy McCall 20 days ago ↶
128
}
129
let ( email, page) = ( f.email.trim ( ) , f.page.trim ( ) ) ;
130
if ! email.is_empty ( ) & & contact_uri ( email) .is_none_or ( | u | ! u.starts_with ( " mailto:" ) ) {
131
return Err ( Error:: bad( shell, " The reporting address is an email address, or empty." ) ) ;
132
}
133
if page.chars ( ) .count ( ) > MAX_PAGE {
134
return Err ( Error:: bad( shell, format! ( " The page text is at most {MAX_PAGE} characters." ) ) ) ;
3dd6582d Andy McCall 21 days ago ↶
135
}
136
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
137
accounts:: set_setting( & app.db, CONTACT , contact) .await.map_err ( e) ? ;
138
accounts:: set_setting( & app.db, POLICY , policy) .await.map_err ( e) ? ;
2cbf6f34 Andy McCall 20 days ago ↶
139
accounts:: set_setting( & app.db, EMAIL , email) .await.map_err ( e) ? ;
140
accounts:: set_setting( & app.db, PAGE , page) .await.map_err ( e) ? ;
3dd6582d Andy McCall 21 days ago ↶
141
accounts:: audit( & app.db, Some ( & me) , " admin.security_contact" , contact, policy) .await.ok ( ) ;
142
let notice = if contact.is_empty ( ) {
143
" No security contact: /.well-known/security.txt is gone." .to_string ( )
144
} else {
145
" Security contact saved: /.well-known/security.txt names it." .to_string ( )
146
} ;
147
super :: admin:: admin_page( & app, shell, Some ( notice) ) .await
148
}
149
150
# [ cfg ( test ) ]
151
mod tests {
152
use super :: * ;
153
154
# [ test ]
155
fn contacts_become_uris_or_are_refused ( ) {
156
assert_eq! ( contact_uri ( " [email protected] " ) .as_deref ( ) , Some ( " mailto:[email protected] " ) ) ;
157
assert_eq! ( contact_uri ( " https://example.org/report " ) .as_deref ( ) , Some ( " https://example.org/report" ) ) ;
158
assert_eq! ( contact_uri ( " mailto:[email protected] " ) .as_deref ( ) , Some ( " mailto:[email protected] " ) ) ;
159
for bad in [ " " , " http://example.org" , " nobody" , " a@b" , " a [email protected] " , " javascript:alert(1)" , " [email protected] \n Contact: evil" ] {
160
assert_eq! ( contact_uri ( bad) , None , " {bad:?}" ) ;
161
}
162
}
163
164
# [ test ]
165
fn the_file_expires_under_a_year_ahead_and_is_canonical ( ) {
166
let now = 1_800_000_000_000 ;
167
let t = text ( " https://hub.example/" , " [email protected] " , " https://hub.example/policy" , now) .unwrap ( ) ;
168
assert_eq! (
169
t,
170
" Contact: mailto:[email protected] \n Expires: 2028-01-10T08:00:00Z\n Policy: https://hub.example/policy\n \
171
Preferred-Languages: en\n Canonical: https://hub.example/.well-known/security.txt\n "
172
) ;
173
assert! ( ! text ( " https://hub.example" , " [email protected] " , " " , now) .unwrap ( ) .contains ( " Policy" ) ) ;
174
assert! ( text ( " https://hub.example" , " " , " " , now) .is_none ( ) ) ;
175
}
176
}