Skip to content

Commit

Permalink
core: add security.txt support (#3458)
Browse files Browse the repository at this point in the history
* core: add security.txt support

* Add 'mailto:' before email address
  • Loading branch information
mworrell committed Jun 27, 2023
1 parent 0f5fa72 commit 09f7aea
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 1 deletion.
7 changes: 6 additions & 1 deletion modules/mod_base/dispatch/dispatch
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,10 @@
{jslog, ["log-client-event"], controller_nocontent, [{ssl, any}]},

%% robots.txt - simple allow all file
{robots_txt, ["robots.txt"], controller_file, [ {path, "misc/robots.txt"}, {root,[lib]}, {content_disposition, inline} ]}
{robots_txt, ["robots.txt"], controller_file, [ {path, "misc/robots.txt"}, {root,[lib]}, {content_disposition, inline} ]},

%% security.txt - serve a template with the security settings - https://securitytxt.org
{security_txt, [".well-known", "security.txt"], controller_template, [
{template, "security.txt.tpl"}, {content_type, "text/plain"}, {anonymous, true}
]}
].
24 changes: 24 additions & 0 deletions modules/mod_base/templates/security.txt.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% with m.site.security as security %}
# Our security address
{% for k, c in security.contact %}
Contact: {% if k == 'email' %}mailto:{% endif %}{{ c }}
{% empty %}
# Please contact the site administrator as no contact details are configured for security issues specifically.
{% endfor %}

{% if security.policy as url %}
# Our security policy
Policy: {{ url }}
{% endif %}

{% if security.hiring as url %}
# We are hiring!
Hiring: {{ url }}
{% endif %}

# The official location of this file
Canonical: {% url security_txt use_absolute_url %}

Expires: {{ security.expires }}

{% endwith %}
6 changes: 6 additions & 0 deletions priv/zotonic.config.in
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
%%% Default timezone (for example: <<"Europe/Berlin">>)
%% {timezone, <<"UTC">>},

%%% Global security.txt settings
%% {security_email, <<"security@example.com">>},
%% {security_url, <<"https://example.com/security">>},
%% {security_policy_url, <<"https://example.com/security-policy">>},
%% {security_hiring_url, <<"https://example.com/security-hiring">>},

%%% PostgreSQL database defaults.
%%% These are the defaults for the equally named options in your site's config file.
{dbdatabase, "zotonic"},
Expand Down
87 changes: 87 additions & 0 deletions src/models/m_site.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
m_value/2,

environment/1,
security/1,
all/1,
get/2,
get/3
Expand All @@ -47,6 +48,8 @@ m_find_value(protocol, #m{value=undefined}, Context) ->
z_context:site_protocol(Context);
m_find_value(environment, _M, Context) ->
environment(Context);
m_find_value(security, _M, Context) ->
security(Context);
m_find_value(Key, #m{value=undefined}, Context) ->
get(Key, Context).

Expand Down Expand Up @@ -136,3 +139,87 @@ get(Module, Key, Context) when is_atom(Key) ->
undefined -> undefined;
L when is_list(L) -> proplists:get_value(Key, L)
end.



%% @doc Return the security.txt configuration. See also https://securitytxt.org
-spec security(Context) -> SecurityConfig when
Context :: z:context(),
SecurityConfig :: proplists:proplist().
security(Context) ->
ContactEmail = security_contact_email(Context),
Contact1 = if
ContactEmail =:= undefined -> [];
true -> [ {email, ContactEmail} ]
end,
ContactUrl = security_contact_url(Context),
Contact2 = if
ContactUrl =:= undefined -> Contact1;
true -> [ {url, ContactUrl} | Contact1 ]
end,
Sec = [ {contact, Contact2} ],
PolicyUrl = security_policy(Context),
Sec1 = if
PolicyUrl =:= undefined -> Sec;
true -> [ {policy, PolicyUrl} | Sec ]
end,
HiringUrl = security_hiring(Context),
Sec2 = if
HiringUrl =:= undefined -> Sec1;
true -> [ {hiring, HiringUrl} | Sec1 ]
end,
[ {expires, security_expires(Context)} | Sec2 ].

security_expires(Context) ->
Expires = case m_config:get_value(site, security_expires, Context) of
None when None =:= undefined; None =:= <<>> ->
z_datetime:next_month(erlang:universaltime());
Exp ->
z_datetime:to_datetime(Exp)
end,
erlydtl_dateformat:format_utc(Expires, "c", Context).


security_contact_email(Context) ->
case m_config:get_value(site, security_email, Context) of
None when None =:= undefined; None =:= <<>> ->
case z_config:get(security_email) of
undefined -> m_rsc:p(1, email_raw, Context);
E -> E
end;
E ->
E
end.

security_contact_url(Context) ->
case m_config:get_value(site, security_url, Context) of
None1 when None1 =:= undefined; None1 =:= <<>> ->
case m_rsc:p(<<"page_security_contact">>, page_url_abs, Context) of
undefined -> z_config:get(security_url);
P -> P
end;
P ->
P
end.

security_policy(Context) ->
case m_config:get_value(site, security_policy_url, Context) of
None when None =:= undefined; None =:= <<>> ->
case m_rsc:p(<<"page_security_policy">>, page_url_abs, Context) of
undefined -> z_config:get(security_policy_url);
P -> P
end;
P ->
P
end.

security_hiring(Context) ->
case m_config:get_value(site, security_hiring_url, Context) of
None when None =:= undefined; None =:= <<>> ->
case m_rsc:p(<<"page_security_hiring">>, page_url_abs, Context) of
undefined -> z_config:get(security_hiring_url);
P -> P
end;
P ->
P
end.

0 comments on commit 09f7aea

Please sign in to comment.