天天看點

審閱 “ 史上 ” 最爛的代碼

來源:infoQ

https://reurl.cc/2b4NGv

Facebook 上有一個名為“Il Programmatore di Merda”(翻譯為“ The Shitty Programmer”,中文含義為“糟糕的程式猿”)的社群, 我經常去浏覽。網站經常分享一些糟糕的代碼和有關程式設計的話題。今天,我看到一段令我難以置信的代碼:

審閱 “ 史上 ” 最爛的代碼

本周最爛代碼

仔細看看,上面的代碼錯誤太多,以至于我不知從何談起。

如果你是一個初級開發工程師,這篇文章會幫你明白上述代碼中存在的一些非常嚴重的問題,并讓你引以為鑒。

128 行錯誤代碼

我把上面的代碼摘錄下來,以便我們進行後面的讨論:

<script>
function authenticateUser(username, password) {
  var accounts = apiService.sql(
    "SELECT * FROM users"
  );


  for (var i = 0; i < accounts.length; i++) {
    var account = accounts [i];
    if (account.username === username &&
        account.password === password)
    {
      return true;
    }
  }
  if ("true" === "true") {
    return false;
  }
}


$('#login').click(function() {
  var username = $("#username").val();
  var password = $("#password").val();


  var authenticated = authenticateUser(username, password);


  if (authenticated === true) {
    $.cookie('loggedin', 'yes', { expires: 1 });
  } else if (authenticated === false) {
    $("error_message").show(LogInFailed);
  }
});
</script>
           

一時之間,我竟不知道從何說起。

上述錯誤大緻分為 3 類:

  • 安全問題
  • 基本程式設計概念問題
  • 代碼格式化問題

2安全問題

我們非常确定以下代碼會在用戶端運作,因為它被包裝在兩個

<script>

标記間(當然,它使用 jQuery 程式設計架構)。不要誤會我的意思,這些代碼即使是運作在伺服器端也很糟糕,在用戶端上運作這些代碼會将你的資料庫暴露給……每個人。

讓我們先看一下

authenticateUser

函數:

function authenticateUser(username, password) {
  var accounts = apiService.sql(
    "SELECT * FROM users"
  );


  for (var i = 0; i < accounts.length; i++) {
    var account = accounts [i];
    if (account.username === username &&
        account.password === password)
    {
      return true;
    }
  }
  if ("true" === "true") {
    return false;
  }
}
           

我們的代碼在某些地方有個叫做

apiServices

的接口,它公開了一個

.sql

方法,可以對資料庫進行 SQL 操作。這意味着,如果你在運作上述代碼的浏覽器上打開控制台,就可以執行各種查詢,安全隐患極高。比如,你無需獲得授權就可以這樣做:

apiService.sql("show tables;");
           

調用上述 API,代碼執行後會傳回資料庫的所有表名稱。

我們暫且假裝這不是一個嚴重的問題。但是請接着往下看:

if (account.username === username &&
    account.password === password)
           

是以,作者的意思是直接儲存了使用者所有的明文密碼,而沒有對它們進行哈希處理?

這簡直不可思議!現在我可以打開 Chrome 浏覽器的調試器,直接檢視每個使用者的明文密碼。

我非常确定,很大一部分使用者會在社交網絡、電子郵件服務、銀行賬戶等服務中使用相同的使用者名和密碼,想象一下,别人可以在沒有任何障礙下就可以拿到你的賬戶和密碼,這得有多可怕。

作者嘗試設定

登入

cookie 的方式也存在問題:

$.cookie('loggedin', 'yes', { expires: 1 });
           

是以按照代碼的意思,作者使用 jQuery 設定 cookie,讓該 cookie 告知 Web 應用程式使用者是否通過身份驗證。

好吧,千萬不要使用 JavaScript 來設定此類 cookie。

如果你有存儲此類登陸資訊的需求,那麼使用 cookie 确實是最常見的解決方案,這沒有什麼問題!但是使用 JavaScript 設定它們意味着你無法設定

httpOnly

屬性,這會導緻每個惡意腳本都能輕而易舉地通路和擷取你的 cookie 内容。

是的,我知道,他們隻是存儲

'loggedin': 'yes'

的鍵值資訊,可能不是上面我講的那種情況,但總之這是一個糟糕的做法。

另外,打開 Chrome 控制台,我随時可以輸入

$ .cookie('loggedin','yes',{expires: 1000000000000})

指令, 而且即使我沒有使用者帳戶,也會永遠保持登入狀态。

3基本程式設計概念問題

想說的話太多,但無奈時間有限。

很明顯,

authenticateUser

函數寫的就是一堆垃圾,該函數的實作充分表明作者缺乏一些基本的程式設計概念。

function authenticateUser(username, password) {
  var accounts = apiService.sql(
    "SELECT * FROM users"
  );


  for (var i = 0; i < accounts.length; i++) {
    var account = accounts [i];
    if (account.username === username &&
        account.password === password)
    {
      return true;
    }
  }
  if ("true" === "true") {
    return false;
  }
}
           

代碼作者為什麼不隻查詢給定使用者名和密碼的使用者,而是檢索出資料庫中的所有使用者呢?如果該資料庫中擁有數百萬個使用者怎麼辦?

還有前面我已經說過了,在這裡我再提一下,為什麼作者不對資料庫中的明文密碼進行哈希處理?

讓我們接着看一下

authenticateUser

函數的傳回值。

我們可以看到,該函數接收兩個 string 類型的參數,最後傳回一個布爾類型的值。是以,下面的代碼即使很糟糕(明文密碼),但也有一定的意義:

for (var i = 0; i < accounts.length; i++) {
  var account = accounts [i];
  if (account.username === username &&
      account.password === password)
  {
    return true;
  }
}
           

上面代碼的含義很清楚,“是否存在具有 X 使用者名和 Y 密碼的使用者?是的,是以函數執行結果傳回 true”。

但是下面這個代碼:

if ("true" === "true") {
  return false;
}
           

這根本沒有任何道理呀。

為什麼該函數不去掉

always-true

條件判斷,直接傳回 false?

現在,我們繼續接着分析後面的代碼:

$('#login').click(function() {
  var username = $("#username").val();
  var password = $("#password").val();


  var authenticated = authenticateUser(username, password);


  if (authenticated === true) {
    $.cookie('loggedin', 'yes', { expires: 1 });
  } else if (authenticated === false) {
    $("error_message").show(LogInFailed);
  }
});
           

使用 jQuery 擷取屬性值的代碼部分沒有什麼問題。問題在于它如何處理

loggedin

使用者的 cookie。

我們之前讨論過這樣一個問題,我可以在我的 Chrome 控制台輸入

$ .cookie('loggedin','yes',{expires:1});

保持認證一整天,甚至都不需要一個帳戶。

是以,這個網站到底是怎麼确定我是誰的?也許它隻是通過使用者名 / 密碼身份驗證顯示一些私人内容,是以它沒有展示任何個人資料。總之,沒有人知道代碼為什麼會這麼寫。

4代碼格式化問題

代碼格式可能是整個代碼中不太重要的部分,但我們可以很容易地判斷出該開發人員複制 / 粘貼了某些網站上的代碼。

下面的代碼片段,我們可以看到開發者使用了雙引号引用字元串:

var username = $("#username").val();
var password = $("#password").val();
           

然而,下面的代碼卻又使用了單引号字元串:

$.cookie('loggedin', 'yes', { expires: 1 });
           

這些看起來可能沒有那麼重要,但實際上我們可以确定,開發人員可能已經從 StackOverflow 複制粘貼了一些代碼,甚至都沒有遵循整個代碼庫的代碼規範來重寫它們。當然,這隻是一個小問題,但它表明開發人員并不真正關心和了解代碼的工作方式,隻是希望代碼以某種方式工作。

大家不要誤會,我每天都會在 Google 上進行搜尋,但比起僅僅複制和粘貼代碼來實作功能,了解代碼的工作原理——比如了解如何設定 Cookie,實際上更為重要。如果由于某種原因整個程序中斷了怎麼辦?你如何确定是腳本的哪一部分不起作用呢?

5總結

我絕對可以确定上面的代碼是僞造的。這是我第一次看到使用同步方式進行 SQL 查詢:

var accounts = apiService.sql(
  "SELECT * FROM users"
);
           

通常,我希望查詢功能的實作類似下面這樣:

var accounts = apiService.sql("SELECT * FROM users", (err, res) => {
  console.log(err); // some error
  console.log(res); // query result
});
           

或者這樣:

var accounts = await apiService.sql(
  "SELECT * FROM users"
);
           

即使使用同步方式調用

apiService.sql

傳回查詢值(我對此表示懷疑),在内部也必須進行與資料庫的連接配接、執行查詢語句并發送傳回查詢結果,這些過程(你可能已經知道了)明顯是不同步的。

但是,即使上面的代碼不是僞造的,我也可以确信它是由初級開發人員編寫的。我剛剛開始入行寫代碼的一段時間裡,我很确定自己為之前的公司也寫過這麼糟糕的代碼。

這個鍋不能甩給初級開發人員。

讓我們假設上面的代碼是真實的。這裡的初級開發人員正在竭盡所能實作功能。他 / 她尚未開始學習如何正确處理 SQL 查詢、cookie 以及其他需要注意的技術點,這完全可以了解!

進階開發人員應該提供某種形式的指導,以確定初級開發人員可以了解他們的錯誤,保證這樣的錯誤代碼不會在生産環境中使用。

我也可以确認,有些公司其實并不真正在乎開發人員編寫的代碼品質。

代碼能解決問題嗎?——生産環境部署一下就知道了呀。代碼是由初級開發人員編寫的,甚至都沒有進階開發人員的準許嗎?——部署運作一下就知道結果了呀。

哎,Shit happens!

6後記

我在 Reddit 對此進行了一番讨論後,一個非常給力的小夥伴分享了下面的 Reddit 話題:

“This JavaScript code powers a 1,500 user intranet application”

https://www.reddit.com/r/programminghorror/comments/66klvc/this_javascript_code_powers_a_1500_user_intranet

是以,是我錯了。這段代碼并不是僞造的