Skip to content

Commit

Permalink
Fix/1920 csp (#2003)
Browse files Browse the repository at this point in the history
fix(frontend): CSP more accurate

Define the directives for our CDNs.
More info about it: https://content-security-policy.com/

closes #1920

Co-authored-by: Fernando Verdugo <fernando.verdugo@upc.edu>
  • Loading branch information
frankiejol and fv3rdugo committed Oct 26, 2023
1 parent fb10468 commit 46525a4
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ log
rvd_front.conf
pkg-debian-out
public/img/screenshots
public/js/custom
yarn.lock
node_modules/
t/vm/b10*
Expand Down
3 changes: 3 additions & 0 deletions etc/rvd_front.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@
# Insert widget in /js/custom/insert_here_widget.js
# this widget embed js in templates/bootstrap/scripts.html.ep
,widget => ''
# Content-Security-Policy HTTP response header helps you reduce XSS risks
# define custom directives. More info https://content-security-policy.com/
,security_policy => 'foo.bar.com'
};
53 changes: 53 additions & 0 deletions lib/Ravada.pm
Original file line number Diff line number Diff line change
Expand Up @@ -2543,6 +2543,59 @@ sub _sql_insert_defaults($self){
,name => 'auto_view'
,value => $conf->{auto_view}
}
,{ id_parent => $id_frontend
,name => "widget"
}
,{
id_parent => $id_frontend
,name => 'content_security_policy'
}
,{
id_parent => "/frontend/content_security_policy"
,name => "all"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "default-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "style-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "script-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "object-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "frame-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "font-src"
,value => ''
}

,{
id_parent => "/frontend/content_security_policy"
,name => "connect-src"
,value => ''
}
,{
id_parent => "/frontend/content_security_policy"
,name => "media-src"
,value => ''
}
,{
id_parent => $id_backend
,name => 'start_limit'
Expand Down
19 changes: 17 additions & 2 deletions lib/Ravada/Front.pm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use Hash::Util qw(lock_hash);
use IPC::Run3 qw(run3);
use JSON::XS;
use Moose;
use Storable qw(dclone);
use Ravada;
use Ravada::Auth::LDAP;
use Ravada::Front::Domain;
Expand Down Expand Up @@ -1481,6 +1482,19 @@ sub _settings_by_id($self) {
return $orig_settings;
}

sub _settings_by_parent($self,$parent) {
my $data = $self->_setting_data($parent);
my $sth = $self->_dbh->prepare("SELECT name,value FROM settings "
." WHERE id_parent = ? ");
$sth->execute($data->{id});
my $ret;
while (my ($name, $value) = $sth->fetchrow) {
$value = '' if !defined $value;
$ret->{$name} = $value;
}
return $ret;
}

=head2 feature
Returns if a feature is available
Expand Down Expand Up @@ -1521,9 +1535,10 @@ sub update_settings_global($self, $arg, $user, $reload, $orig_settings = $self->
confess Dumper([$field,$arg->{$field}]) if !ref($arg->{$field});
if ( scalar(keys %{$arg->{$field}})>2 ) {
confess if !keys %{$arg->{$field}};
$self->update_settings_global($arg->{$field}, $user, $reload, $orig_settings);
my $field2 = dclone($arg->{$field});
$self->update_settings_global($field2, $user, $reload, $orig_settings);
}
confess "Error: invalid field $field" if $field !~ /^\w+$/;
confess "Error: invalid field $field" if $field !~ /^\w[\w\-]+$/;
my ( $value, $id )
= ($arg->{$field}->{value}
, $arg->{$field}->{id}
Expand Down
25 changes: 24 additions & 1 deletion public/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1453,7 +1453,27 @@ ravadaApp.directive("solShowMachine", swMach)

function settings_global_ctrl($scope, $http) {
$scope.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
$scope.init = function() {
$scope.csp_locked = false;
$scope.set_csp_locked=function() {
var keys = Object.keys($scope.settings.frontend.content_security_policy);
var found = 0;
for ( var n_key=0 ; n_key<keys.length ; n_key++) {
var field=keys[n_key];
if ( field != 'all' && field != 'id' && field != 'value'
&& $scope.settings.frontend.content_security_policy[field].value) {
found++;
}
}
$scope.csp_locked = found>0;
if ($scope.csp_locked && !$scope.csp_advanced) {
$scope.csp_advanced = true;
}
};
$scope.init = function(url, csp_advanced) {
$scope.csp_advanced=false;
if (csp_advanced) {
$scope.csp_advanced=true;
}
$http.get('/settings_global.json').then(function(response) {
$scope.settings = response.data;
var now = new Date();
Expand All @@ -1472,17 +1492,20 @@ ravadaApp.directive("solShowMachine", swMach)
$scope.settings.frontend.maintenance_end.value
=new Date($scope.settings.frontend.maintenance_end.value);
}
$scope.set_csp_locked();
});
};
$scope.load_settings = function() {
$scope.init();
$scope.set_csp_locked();
$scope.formSettings.$setPristine();
};
$scope.update_settings = function() {
$scope.formSettings.$setPristine();
$http.post('/settings_global'
,JSON.stringify($scope.settings)
).then(function(response) {
$scope.set_csp_locked();
if (response.data.reload) {
window.location.reload();
}
Expand Down
48 changes: 41 additions & 7 deletions script/rvd_front
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,50 @@ sub _time() {
return strftime('%Y/%m/%d:%H:%M:%S %z',localtime);
}

sub _security_policy() {

my $config=$RAVADA->_settings_by_parent("/frontend/content_security_policy");
my $all = ($config->{all} or '');
my %src = (
"default-src" => "'self' sha256- sha384 http: https: data:"
,"style-src" => "'self' cdnjs.cloudflare.com stackpath.bootstrapcdn.com cdn.ckeditor.com cdn.jsdelivr.net use.fontawesome.com 'unsafe-inline'"
,"script-src" => "'self' code.jquery.com cdn.ckeditor.com cdnjs.cloudflare.com stackpath.bootstrapcdn.com ajax.googleapis.com cdn.jsdelivr.net 'unsafe-inline' 'unsafe-eval'"
,"object-src" => " 'self'"
,"media-src" => "'self'"
,"frame-src" => "'self'"
,"font-src" => "'self' data: use.fontawesome.com"
,"connect-src" => "'self'"
);
my $sec = '';
for my $field (sort keys %src) {
$sec .= " " if $sec;
$sec .= $field." ".$src{$field};
$sec .= " $all" if $all;
if ( exists $config->{$field} ) {
$sec .= " ".${config}->{$field};
}
$field =~ s/-/_/g;
if ( exists $config->{$field} ) {
$sec .= " ".$config->{$field};
}

$sec .= ";";
}
return $sec;
}

hook before_routes => sub {
my $c = shift;

$c ->res->headers->content_security_policy (
"object-src 'auto';"
."media-src 'self';"
." frame-src 'self';"
." connect-src 'self'; ");

my $sec = _security_policy();
$c ->res->headers->content_security_policy($sec);

$USER = undef;

$c->stash(version => $RAVADA->version);
my $url = $c->req->url->to_abs->path;
my $host = $c->req->url->to_abs->host;
my $widget=( $CONFIG_FRONT->{widget} or $RAVADA->setting('/frontend/widget'));
$c->stash(css=>['/css/sb-admin.css']
,js_mod=>[ ## angular modules
'/js/booking/booking.module.js?v='.$RAVADA->version
Expand All @@ -184,7 +213,7 @@ hook before_routes => sub {
,host => $host
,bookings => $RAVADA->setting('/backend/bookings')
,FEATURE => {}
,widget => $CONFIG_FRONT->{widget}
,widget => $widget
);

$USER = _logged_in($c);
Expand Down Expand Up @@ -2810,6 +2839,11 @@ sub admin {
return access_denied($c) unless $USER->is_admin;
my $url = $c->req->url->to_abs->path;
my $host = $c->req->url->to_abs->host;
my $csp = $RAVADA->_settings_by_parent("/frontend/content_security_policy");
my $csp_advanced = 0;
$csp_advanced = grep $csp->{$_}, grep /-/,keys %$csp;

$c->stash( csp => $csp , csp_advanced => $csp_advanced);
$c->stash(url_login => "/login");
}
if ($page eq 'storage') {
Expand Down
85 changes: 85 additions & 0 deletions t/mojo/40_security_policy.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use warnings;
use strict;

use Carp qw(confess);
use Data::Dumper;
use HTML::Lint;
use Test::More;
use Test::Mojo;
use Mojo::File 'path';
use Mojo::JSON qw(decode_json);
use Storable qw(dclone);

use lib 't/lib';
use Test::Ravada;

no warnings "experimental::signatures";
use feature qw(signatures);

my $SECONDS_TIMEOUT = 15;

my $t;

my $URL_LOGOUT = '/logout';
my ($USERNAME, $PASSWORD) = (user_admin->name, "$$ $$");
my $SCRIPT = path(__FILE__)->dirname->sibling('../script/rvd_front');

$ENV{MOJO_MODE} = 'devel';
init('/etc/ravada.conf',0);
my $connector = rvd_back->connector;
like($connector->{driver} , qr/mysql/i) or BAIL_OUT;

$Test::Ravada::BACKGROUND=1;

$t = Test::Mojo->new($SCRIPT);
$t->ua->inactivity_timeout(900);
$t->ua->connect_timeout(60);

mojo_login($t, $USERNAME, $PASSWORD);

my $sth = rvd_front->_dbh->prepare("UPDATE settings set value='' WHERE id_parent=?");

$t->get_ok("/settings_global.json")->status_is(200);
my $body = $t->tx->res->body();
my $settings = decode_json($body);

$sth->execute($settings->{frontend}->{content_security_policy}->{id});

my $new = dclone($settings);
my $exp_default = "foodefault.example.com";
my $exp_all = "fooall.example.com";
$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = $exp_default;
$new->{frontend}->{content_security_policy}->{'all'}->{value} = $exp_all;
delete $new->{backend};

my $reload=0;
rvd_front->update_settings_global($new,user_admin,$reload);

$t->post_ok("/settings_global", json => $new );

$t->get_ok("/settings_global.json")->status_is(200);
$body = $t->tx->res->body();
my $settings2 = decode_json($body);
is($settings2->{frontend}->{content_security_policy}->{'all'}->{value} , $exp_all) or exit;
is($settings2->{frontend}->{content_security_policy}->{'default-src'}->{value} , $exp_default) or exit;

my $config_csp = rvd_front->_settings_by_parent("/frontend/content_security_policy");
is($config_csp->{all}, $exp_all);
is($config_csp->{'default-src'}, $exp_default);

my $header = $t->tx->res->headers->content_security_policy();
my %csp;
for my $entry (split /;/,$header) {
my ($key,$value) = $entry =~ /\s*(.*?)\s+(.*)/;
$csp{$key}=$value;
}

like($csp{'default-src'},qr/$exp_all/);
like($csp{'default-src'},qr/$exp_default/);

$sth->execute($settings->{frontend}->{content_security_policy}->{id});

$new->{frontend}->{content_security_policy}->{'default-src'}->{value} = '';
$new->{frontend}->{content_security_policy}->{'all'}->{value} = '';
$t->post_ok("/settings_global", json => $new );
done_testing();
2 changes: 1 addition & 1 deletion templates/bootstrap/header.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<title>Ravada VDI</title>

% if ( !$fallback ) {
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css">
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ui-bootstrap4@3.0.6/dist/ui-bootstrap-csp.css">
<link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet">
Expand Down
Loading

0 comments on commit 46525a4

Please sign in to comment.