RT#29354: Password Security in Email [xmlhttp validation for selfservice]
authorJonathan Prykop <jonathan@freeside.biz>
Wed, 2 Dec 2015 11:02:17 +0000 (05:02 -0600)
committerJonathan Prykop <jonathan@freeside.biz>
Tue, 15 Dec 2015 04:38:35 +0000 (22:38 -0600)
FS/FS/ClientAPI/MyAccount.pm
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/add_password_validation.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/change_password.html
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/signup.cgi
fs_selfservice/FS-SelfService/cgi/signup.html
fs_selfservice/FS-SelfService/cgi/validate_password.html [new file with mode: 0644]
httemplate/elements/change_password.html

index f50b9f1..3364821 100644 (file)
@@ -3245,6 +3245,45 @@ sub process_reset_passwd {
 
 }
 
+sub validate_passwd {
+  my $p = shift;
+
+  my %result;
+  %result = ( 'fieldid' => $p->{'fieldid'} )
+    if $p->{'fieldid'} =~ /^\w+$/;
+
+  return { %result, 'password_invalid' => 'Enter new password' }
+    unless length($p->{'check_password'});
+
+  my $svc_acct;
+  if ($p->{'svcnum'}) {
+    # false laziness with myaccount_passwd
+    my($context, $session, $custnum) = _custoragent_session_custnum($p);
+    return { %result, 'error' => $session } if $context eq 'error';
+
+    $custnum =~ /^(\d+)$/ or die "illegal custnum";
+    my $search = " AND custnum = $1";
+    $search .= " AND agentnum = ". $session->{'agentnum'} if $context eq 'agent';
+
+    $svc_acct = qsearchs( {
+      'table'     => 'svc_acct',
+      'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  ) '.
+                     'LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
+                     'LEFT JOIN cust_main USING ( custnum ) ',
+      'hashref'   => { 'svcnum' => $p->{'svcnum'}, },
+      'extra_sql' => $search, #important
+    } )
+      or return { %result, 'error' => "Service not found" };
+    # end false laziness
+  }
+
+  $svc_acct ||= new FS::svc_acct {};
+
+  my $error = $svc_acct->is_password_allowed($p->{'check_password'});
+  return { %result, 'password_invalid' => $error } if $error;
+  return { %result, 'password_valid' => 1 };
+}
+
 sub list_tickets {
   my $p = shift;
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
index 9764ad4..7379452 100644 (file)
@@ -95,6 +95,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'reset_passwd'              => 'MyAccount/reset_passwd',
   'check_reset_passwd'        => 'MyAccount/check_reset_passwd',
   'process_reset_passwd'      => 'MyAccount/process_reset_passwd',
+  'validate_passwd'           => 'MyAccount/validate_passwd',
   'list_tickets'              => 'MyAccount/list_tickets',
   'create_ticket'             => 'MyAccount/create_ticket',
   'get_ticket'                => 'MyAccount/get_ticket',
diff --git a/fs_selfservice/FS-SelfService/cgi/add_password_validation.html b/fs_selfservice/FS-SelfService/cgi/add_password_validation.html
new file mode 100644 (file)
index 0000000..e349fd7
--- /dev/null
@@ -0,0 +1,36 @@
+<SCRIPT>
+function add_password_validation (fieldid) {
+  var inputfield = document.getElementById(fieldid);
+  inputfield.onchange = function () {
+    var fieldid = this.id+'_result';
+    var resultfield = document.getElementById(fieldid);
+    var svcnum = '';
+    var svcfield = document.getElementById(this.id+'_svcnum');
+    if (svcfield) {
+      svcnum = svcfield.options[svcfield.selectedIndex].value;
+    }
+    if (this.value) {
+      resultfield.innerHTML = '<SPAN STYLE="color: blue;">Validating password...</SPAN>';
+      send_xmlhttp('selfservice.cgi',
+        ['action','validate_password','fieldid',fieldid,'svcnum',svcnum,'check_password',this.value],
+        function (result) {
+          result = JSON.parse(result);
+          var resultfield = document.getElementById(result.fieldid);
+          if (resultfield) {
+            if (result.valid) {
+              resultfield.innerHTML = '<SPAN STYLE="color: green;">Password valid!</SPAN>';
+            } else if (result.error) {
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.error+'</SPAN>';
+            } else {
+              result.syserror = result.syserror || 'Server error';
+              resultfield.innerHTML = '<SPAN STYLE="color: red;">'+result.syserror+'</SPAN>';
+            }
+          }
+        }
+      );
+    } else {
+      resultfield.innerHTML = '';
+    }
+  };
+}
+</SCRIPT>
index 22d8973..ef66554 100644 (file)
@@ -12,7 +12,7 @@
   <TR>
     <TH ALIGN="right">Change password for account: </TH>
     <TD>
-      <SELECT NAME="svcnum">
+      <SELECT ID="new_password_svcnum" NAME="svcnum">
         <%= foreach my $svc ( @svcs ) {
               $OUT .= '<OPTION VALUE="'. $svc->{'svcnum'}. '"'.
                         ( $svc->{'svcnum'} eq $svcnum ? ' SELECTED' : '' ). '>'.
 
   <TR>
     <TH ALIGN="right">New password: </TH>
-    <TD><INPUT TYPE="password" NAME="new_password" SIZE="18"></TD>
+    <TD>
+      <INPUT ID="new_password" TYPE="password" NAME="new_password" SIZE="18">
+      <DIV ID="new_password_result"></DIV>
+<%= include('send_xmlhttp') %>
+<%= include('add_password_validation') %>
+<SCRIPT>
+add_password_validation('new_password');
+</SCRIPT>
+    </TD>
   </TR>
 
   <TR>
index 4199f70..f6f3c21 100755 (executable)
@@ -23,6 +23,7 @@ use FS::SelfService qw(
   mason_comp port_graph
   start_thirdparty finish_thirdparty
   reset_passwd check_reset_passwd process_reset_passwd
+  validate_passwd
   billing_history
 );
 
@@ -84,6 +85,7 @@ my @actions = ( qw(
   customer_suspend_pkg
   process_suspend_pkg
   history
+  validate_password
 ));
 
 my @nologin_actions = (qw(
@@ -108,7 +110,6 @@ if ( $cgi->param('action') =~ /^process_forgot_password_session_(\w+)$/ ) {
     warn "WARNING: unrecognized action '$1'\n";
   }
 }
-
 unless ( $nologin_actions{$action} ) {
 
   my %cookies = CGI::Cookie->fetch;
@@ -1109,6 +1110,14 @@ sub do_process_forgot_password {
   );
 }
 
+sub validate_password {
+  validate_passwd(
+    'session_id' => $session_id,
+    map { $_ => scalar($cgi->param($_)) }
+      qw( fieldid svcnum check_password )
+  )
+}
+
 #--
 
 sub do_template {
diff --git a/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html b/fs_selfservice/FS-SelfService/cgi/send_xmlhttp.html
new file mode 100644 (file)
index 0000000..ac85cb2
--- /dev/null
@@ -0,0 +1,45 @@
+<SCRIPT>
+function rs_init_object () {
+  var A;
+  try {
+    A=new ActiveXObject("Msxml2.XMLHTTP");
+  } catch (e) {
+    try {
+      A=new ActiveXObject("Microsoft.XMLHTTP");
+    } catch (oc) {
+      A=null;
+    }
+  }
+  if(!A && typeof XMLHttpRequest != "undefined")
+    A = new XMLHttpRequest();
+  if (!A)
+    alert("Can't create XMLHttpRequest object");
+  return A;
+}
+
+function send_xmlhttp (url,args,callback) {
+  args = args || [];
+  callback = callback || function (data) { return data };
+  var content = '';
+  for (var i = 0; i < args.length; i = i + 2) {
+    content = content + "&" + args[i] + "=" + escape(args[i+1]);
+  }
+  content = content.replace( /[+]/g, '%2B'); // fix unescaped plus signs 
+
+  var xmlhttp = rs_init_object();
+  xmlhttp.open("POST", url, true);
+
+  xmlhttp.onreadystatechange = function() {
+    if (xmlhttp.readyState != 4) 
+      return;
+    if (xmlhttp.status == 200) {
+      var data = xmlhttp.responseText;
+      callback(data);
+    }
+  };
+
+  xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+  xmlhttp.send(content);
+}
+</SCRIPT>
+
index 817fdd3..072ce96 100755 (executable)
@@ -508,3 +508,31 @@ use FS::SelfService qw( regionselector expselect popselector domainselector
                         didselector
                       );
 
+sub add_password_validation {
+  my $fieldid = shift;
+  my $out = '';
+  if ((-e './send_xmlhttp.html') && (-e './add_password_validation.html')) {
+    my $template = new Text::Template( TYPE   => 'FILE',
+                                       SOURCE => "./send_xmlhttp.html",
+                                       DELIMITERS => [ '<%=', '%>' ],
+                                       UNTAINT => 1,                   
+                                     )
+      or die $Text::Template::ERROR;
+    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
+    $template = new Text::Template( TYPE   => 'FILE',
+                                       SOURCE => "./add_password_validation.html",
+                                       DELIMITERS => [ '<%=', '%>' ],
+                                       UNTAINT => 1,                   
+                                     )
+      or die $Text::Template::ERROR;
+    $out .= $template->fill_in( PACKAGE => 'FS::SelfService::_signupcgi' );
+    $out .= <<ENDOUT;
+<SCRIPT>
+add_password_validation('$fieldid');
+</SCRIPT>
+ENDOUT
+  }
+  return $out;
+}
+
+
index 2bc59ca..5900ba6 100755 (executable)
@@ -336,7 +336,8 @@ HTML::Widgets::SelectLayers->new(
 <FORM name="signup_form" action="<%= $self_url %>" METHOD="POST" onsubmit="return fixup_form();"><BR><FONT SIZE="+1"><B>First package</B></FONT>
 <INPUT TYPE="hidden" NAME="promo_code" VALUE="<%= $promo_code %>">
 <INPUT TYPE="hidden" NAME="reg_code" VALUE="<%= $reg_code %>">
-<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<DIV STYLE="background: <%= $box_bgcolor %>; width: 100%">
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=0>
 <TR>
   <TD COLSPAN=2><SELECT NAME="pkgpart">
 
@@ -383,7 +384,15 @@ ENDOUT
     $OUT .= <<ENDOUT;
 <TR>
   <TD ALIGN="right">Password</TD>
-  <TD><INPUT TYPE="password" NAME="_password" VALUE="$_password"></TD>
+  <TD>
+    <INPUT ID="new_password" TYPE="password" NAME="_password" VALUE="$_password">
+    <DIV ID="new_password_result"></DIV>
+ENDOUT
+
+   $OUT .= add_password_validation('new_password');
+
+   $OUT .= <<ENDOUT;
+  </TD>
 </TR>
 <TR>
   <TD ALIGN="right">Re-enter Password</TD>
@@ -433,6 +442,7 @@ NOMADIX
 %>
 
 </TABLE>
+</DIV>
 
 <%= 
 if ( @optional_packages ) { 
diff --git a/fs_selfservice/FS-SelfService/cgi/validate_password.html b/fs_selfservice/FS-SelfService/cgi/validate_password.html
new file mode 100644 (file)
index 0000000..5cc3167
--- /dev/null
@@ -0,0 +1,9 @@
+<%= use JSON; 
+    encode_json({
+      'valid'    => $password_valid ? 1 : 0,
+      'error'    => $password_invalid,
+      'syserror' => $error,
+      'fieldid'  => $fieldid,
+      'password_debug' => $password_debug,
+    }); %>
+
index 7d8daae..2b40ec1 100644 (file)
@@ -8,7 +8,7 @@
   display: none;
 }
 </STYLE>
-<A ID="<%$pre%>link" HREF="#" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
+<A ID="<%$pre%>link" HREF="javascript:void(0)" onclick="<%$pre%>toggle(true)">(<% mt('change') %>)</A>
 <DIV ID="<%$pre%>form" CLASS="passwordbox">
   <FORM METHOD="POST" ACTION="<%$fsurl%>misc/process/change-password.html">
     <INPUT TYPE="hidden" NAME="svcnum" VALUE="<% $svc_acct->svcnum |h%>">