# Web Security 指南 ## Linux - ls 也就是 list directory。他會列出資料夾內的檔案列表,路徑可以使用相對路徑或是絕對路徑。 - 路徑的表達方式分成**相對路徑**以及**絕對路徑**兩種。 - 相對路徑: 當前位置以及目標位置之間的相對位置。(ex: ../dir1/dir3/test) - 絕對位置: 以跟目錄做為參考基礎的路徑,在Linux中,我們會以 **/** 表示根目錄,以 **~** 表示 **/home/<使用者名稱>**,也就是家目錄 (dir1/dir3/test) - find 可以用於搜尋特定檔案 - find <路徑> -iname <名稱> - find . -iname ".cpp" - nano 簡單的文字編輯器 - nano <檔案> - Ctrl + o 可以存檔 - Ctrl + x 離開編譯器 - gcc 編譯器 - gcc test test.cpp 編譯test.cpp檔案 - Linux 權限 - r:可讀 - w:可寫 - x:可執行 利用 **ls -al** 查詢檔案與資料夾權限 ![](https://i.imgur.com/gn75HfB.png) 除了 **rwx** 的表示方式外,也可以使用數字表示權限 | Dec | Binary | rwx | 代表意義 | | -------- | -------- | -------- | -------- | | 0 | 000 | --- | 此檔案沒有任何權限 | | 1 | 001 | --x | 此檔案可以被執行 | | 2 | 010 | -w- | 此檔案可以被寫入 | | 3 | 011 | -wx | 此檔案可以被寫入、執行 | | 4 | 100 | r-- | 此檔案可以被讀取 | | 5 | 101 | r-x | 此檔案可以被讀取、執行 | | 6 | 110 | rw- | 此檔案可以被讀取、寫入 | | 7 | 111 | rwx | 此檔案可以被讀取、寫入、執行 | - chmod 指令 - chmod 也就是 change file mode bits。它可以將檔案的權限更新。如果是要修改資料夾的權限記得要加上 **-r** 我們可以使用 **rwx** 或是 **0~7** 來表達權限 - 使用 **rwx** 可以選擇哪種種類的權限要變更,或是全部一起變更。 | 使用者 | 定義名稱 | | -------- | -------- | | u | 擁有者 | | g | 群組內使用者 | | o | 其他使用者 | | a | 全部使用者 | ## Docker - 為甚麼使用 Docker 作為練習環境 - Docker 可以幫助我們自動化建立應用程式並快速在任何地方執行 ``` sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release mkdir -p /etc/apt/keyrings 加入Docker官方的GPG key curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 設定穩定的儲存庫 echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 安裝Docker以及相關套件 sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin 將目前的使用者加入到 Docker 群組 usermod -aG docker $USER 確認 Docker 是否安裝成功 docker run hello-world ``` ### LAB: 安裝 docker-compose ``` 下載 docker-compose 檔案 curl -L "https://github.com/docker/compose/releases/download/1.27.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 將檔案加入可執行權限 chmod +x /usr/local/bin/docker-compose ``` ### 常見 docker-compoes 指令 | 指令 | 目的 | | -------- | -------- | | docker pull <image 名稱>:<image 版本> | 取得指定版本映像檔(image),不指定為latest | | docker run <image 名稱>:<image 版本> | 執行容器,透過 image 產生容器 | | docker ps | 檢視當前正在執行的容器 | | docker ps -a | 檢視所有執行過的容器列表 | ## 網頁基礎知識 ### 前端後端的差異 - 前端: 透過網頁瀏覽,可看見的元素,如 **HTML、CSS、JavaScript**。 - 後端: 在網站伺服器執行的,看不見的元素,如 **後端邏輯、後端程式語言、PHP、資料庫**。 ### 靜態與動態網頁的區別 - 靜態網頁: 靜態網頁只有前端元素(HTML、CSS、JavaScript),無後端進行互動,網站伺服器會直接回傳已寫好的檔案給瀏覽器。 - 動態網頁: 動態網頁收到來自瀏覽器的請求時,會經由後端程式碼、後端邏輯,進行資料庫的互動,如: 新增、修改、刪除、查詢資料庫內容,根據使用者與網頁的互動給出不同的回應來回傳伺服器。 ## LAB:開啟開發人員工具 開發人員工具介紹 (網頁按F12) | 功能 | 說明 | | -------- | -------- | | Elements | 元素功能,可查看和更改HTML、CSS | | Console | 控制功能,可查看與執行JavaScript | | Network | 網路功能,可查看網路行為、封包內容 | | Performence | 性能功能,可分析網站執行時的性能與優化網站的速度 | | Memory | 記憶體功能,可分析當前記憶體執行狀況 | | Application | 應用功能,可查看所有資訊含 Web SQL、Session、Cache、圖字等 | | Security | 安全功能,可查看網站是否安全與分析SSL憑證 | | Lighthouse | 改善網站的品質,可謂網站評分 | ## JavaScript 基本語法 除了使用 **開發者工具** 中的 Console 功能,也可以使用 VScode 撰寫HMTL利用內嵌語法 <script></script> 中練習 JavaScript。 ### 基本輸出指令 ```javascript= console.log('HelloWorld'); ``` ![](https://i.imgur.com/c40xLEV.png) 這個**undefined**是函數的回傳值。 ```javascript= window.alert('HelloWorld') ``` ![](https://i.imgur.com/ucZH1g9.png) ![](https://i.imgur.com/NXqHY9f.png) ### 輸出到網站內文 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>網站的標題</title> </head> <body> <p id="demo"></p> <script>document.getElementById('demo').textContent = new Date()</script> </body> </html> ``` ![](https://i.imgur.com/mPYDs1O.png) 使用 console 輸入查看 ![](https://i.imgur.com/C9moLOU.png) 可以發現到 **doucument.getElementById()** 可以幫我們拿到指定 **id** 的元素,接 下來透過 textContent 可以修改原本的內容。 textContext 是屬性 (Attribute),也就是物件當前的狀態。 ### 條件判斷 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>title</title> </head> <body> <script> if(window.confirm('請問要用網站導覽嗎?')) { console.log('使用網站導覽'); } else { console.log('不使用網站導覽'); } </script> </body> </html> ``` ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>title</title> </head> <body> <script> var answer = window.prompt('是否用網頁瀏覽 (yes/no)'); if (answer === 'yes') { window.alert('你使用網頁瀏覽'); } else if (answer === 'no') { window.alert('你不使用網頁瀏覽了'); } else { window.alert('請輸入正規的輸入'); } </script> </body> </html> ``` **window.prompt()** 可讓使用者輸出內容,並將輸入的內容回傳。 ### 變數 變數的命名 - 1.變數名稱可以使用**英文字母、底線符號、錢字號** 以及**數字**。 - 2.變數的第一個字不可為數字 - 3.不能使用保留字 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>title</title> </head> <body> <script> var answer = window.confirm('是否使用網頁導覽'); console.log(answer); </script> </body> </html> ``` ### 比較運算子、資料型態 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>title</title> </head> <body> <script> var ans = Math.floor(Math.random() * 6); var guess = parseInt(window.prompt('請輸入0~5的數字')); var message; if (guess === ans) { message = '答對了!'; } else if (guess > ans) { message = '猜錯了! 數字小一點'; } else if (guess < ans) { message = '猜錯了! 數字大一點'; } window.alert(message); </script> </body> </html> ``` **Math.floor()** 以及 **Math.random()**。 **Math.floor()** 會回傳小於等於所給數字的最大整數 **Math.random()** 會回傳一個介於0到1之間的偽隨機小數。不過可以先不用太過深究,只需要先了解這一行會產出一個介在 0~5 之間的隨機整數 **ParseInt()**,可將字串變成整數,因使用 **window.prompt()** 讓使用者輸入字串,在這判別是需要透過轉換型態成數字,才能進行比較。 ### BOM 與 DOM 其實 JavaScipt 並沒有提供操作網頁的方法,這些網頁操作的方法 (例如前面一直使用的 **window.alert()**) 都是瀏覽器提供的,這些由瀏覽器提供的操作方式都是由 **BOM** 以及 **DOM** 這兩個物件所擁有的,簡單歸納瀏覽器上的 JavaScript 實際上包含了 JavaScript 核心、BOM、DOM。 - BOM(Browser Object Model,瀏覽器物件模型) - DOM(Document Object Model,文件物件模型) #### BOM BOM(Browser Object Model,瀏覽器物件模型),可以用來操作瀏覽器內建的功能,是瀏覽器所有功能的核心。這個模型當中最核心的物件就是 **window 物件**。window 物件提供了許多的屬性,包含了 **doucument**、**screen**....等等。 以下列舉常用的 **window** 屬性: | 屬性 | 說明 | | -------- | -------- | | closed | 回傳視窗是否關閉 | | document | document 物件 | | history | history 物件 | | innerheight | 視窗內部高度 | | innerwidth | 視窗內部寬度 | | length | 回傳框架(frame)數量 | | location | location 物件 | | name | 視窗名稱 | | Navigator | Navigator 物件 | | opener | 表示打開窗口window的對象 | | outerheight | 回傳視窗外部高度 | | outerwidth | 回傳視窗外度寬度 | | pageXOffset | 當前頁面對於視窗顯示區左上角的X位置 | | pageYOffset | 當前頁面對於視窗顯示區左上角的Y位置 | | parent | 回傳父視窗 | | Screen | screen 物件 | | self | 視窗本身的引用 | | status | 顯示瀏覽器狀態欄的字串 | | top | 最頂層祖先視窗 | | window | 視窗本身的引用 | window 方法: | 方法 | 說明 | | -------- | -------- | | alert() | 跳出一個警告訊息窗 | | blur() | 移除該視窗焦點 | | close() | 關閉瀏覽器視窗 | | confirm() | 跳出一個有確認與取消按鈕的確認框 | | createPopup() | 建立出一個彈出視窗 | | focus() | 取得焦點 | | moveBy() | 以相對位置移動視窗 | | moveTo() | 移動視窗到指定位置 | | open() | 開啟一個新的瀏覽器視窗 | | print() | 輸出目前窗口內容 | | prompt() | 跳出可輸入訊息的對話 | | resizeBy() | 調整視窗大小 | | scrollBy() | 滾動內容 | | scrollTo() | 滾動內容 | #### DOM DOM(Document Object Model,文件物件模型),會將 HTML 內的所有標籤 (tag) 定義為物件 (object),而這些物件最後會變成一個樹狀圖。 與 BOM 不同,DOM操作的是網頁內容。他提供 JavaScript 一個操作樹狀圖上各個節點的 API。其中最根本的物件就是 **document**。 - 取的節點的方法主要有四種: - 1.根據ID名稱(ID屬性內容) **javascript=document.getElementById('ID名稱')** - 2.根據元素名稱(tag) **javascript=document.getElementByTagName('tag名稱')** - 3.根據名稱(name屬性內容) **javascript=document.getElementByClassName('name名稱')** - 4.根據class名稱 **javascript=document.getElementByClassName('class名稱')** 值得一提的是,ID這個屬性是唯一的,因此 **getElementById()** 取得的內容也會是唯一的 ## 網頁基礎概論 網際網路中有許多通訊協定,而這些通運協定的通稱為 TCP/IP Protocol Suit,簡稱 TCP/IP。TCP/IP 點對點的連線機制,將傳輸資料的過程標準化,透過資訊封裝、定址、傳輸、路由與目的地接收的方式,並將傳輸過程抽象成四個階層分為 ⌜**應用層**⌟、⌜**傳輸層**⌟、⌜**網路層**⌟、⌜**連接層**⌟。 #### 應用層(Application Layer) 為了要讓應用程式能透過網路與其他應用程式通訊,會需要傳輸的訊息換成相對應協定的格式,而應用層就是將訊息轉換格式的層。轉換過後會在封包加上一個應用層的標頭(Header)。例如使用者透過瀏覽器瀏覽網頁,瀏覽器會透過應用層提供的HTTP通訊協定將請求的資料包起來,轉換成HTTP規定的格式。最後再加上應用層Header傳送到下一層。 #### 傳輸層(Transport Layer) 在傳輸層會透過協定,處理資訊傳輸過程的順序為正確並且順利傳達。常見的協定有TCP以及UDP。TCP(Transmission Control Protocol) 為了方便資料傳輸會將資料拆成許多小封包,每個小封包都加上序號以及連接埠號碼。在開始傳輸前會透過 **三方交握(Three-way Handshake)** 確認連接正常。接下來依序傳送封包,過程中持續檢查傳送的往返時延(RTT)確保封包正確傳輸無遺失。這些小封包最後都會再加上TCP封包Header傳送下一層。 #### 網路互連層(Internet Layer) 確認傳送的資料沒問題後,接下來要確定發送的來源以及目的。網路互連層會在封包上加上IP Header,紀錄來源以及目標IP Address,透過路由(Routing)可以找到一條到達該目的地的路徑,再來接到下一層。 #### 網路存取(連接)層 (Network Access(link) Layer) 確認封包內容的正確性以及傳遞的對象、來源後,最後就是要封送封包了。連接層會透過乙太網路將封包傳送到目標。 當目標接受到訊息後,會反方向將封包拆解,一一除去各層的Header。在傳出層也會依照TCP stream重組 HTTP封包,最後再由伺服器處理HTTP請求,然後回傳相對應的回應,回傳也會依據前面的步驟發送。 - **封裝&拆裝** 在進行通訊的時候,請求端會依序從應用層開始向下傳輸,每通過一個階層時就會在封包上加入該階層的標頭(Header),像這樣將封包資訊進行包裝的過程我們稱為 ⌜封裝⌟。 傳輸到接收端後則會依序從連接層向上拆解到應用層,在過程中慢慢去除封包中的標頭(Header),像這樣封包資訊進行拆解的過程我們稱為 ⌜拆裝⌟。 - **TCP v.s. UTP** 前面提到的TCP為了確保傳輸的可靠性,會將每個封包編號、加上連接埠號碼,並透過三向交握確保資料傳到目的地。 而UFP傳輸的過程當中並不會與對方確認資料就直接傳送資料,因此缺乏可靠性。通常UFP會用於傳輸語音、影像等等可容忍遺失部分封包的情節。 ### DNS網域名稱系統(Domain Name System) 在網路互連曾透過IP協定進行傳輸時,會在封包加上目標 IP Address,而IP Address 是由許多數字組成的,對於人類較不方便記憶,因此我們通常會使用 **域名 (Domain Name)** 找到網站。例如: 我們會透過 www.google.com 連接至Google,而不是使用 172.127.160.100 來連接。而將這些域名對應到 IP Address 的正是 DNS,通常在TCP以及UDP當中會通過53通訊埠傳輸查詢結果。 ``` https://mydreamworld.online/index.html ``` mydreamworld.online 代表域名 (Domain)。 另外。常聽到的子域名(Sub-Doamin)則是從網域名稱再加以延伸的部分,可以連接至網站的不同區段。通常會用於建立測試網站,或是分辨不同用途的網頁。舉例來說,在 example.com 這個 domain 當中有部落格的功能,那麼可能使用 blog.example.com 作為子網域 ``` https://blog.mydreamworld.online/index.html ``` blog 代表子域名 (Sub-Domain) 關於網域的資料可以透過 WHOIS (域名資料庫查詢) 找到該網域的所有者、所有者信箱、購買網域的日期等等。可以使用 CLI 或是網站查詢。 Domain Name System (DNS) server 該伺服器提供域名解析的服務。 ### URL 統一資料定位符 URL(Uniform Resiyrce Locator),俗稱網址,如同網頁的門牌,透過網址就可以到達指定的網頁。URL規定的格式如下: ``` [協定類型]://[伺服器位址]:[埠號]/[檔案路徑][檔名]?[查詢]#[片段ID] ``` ``` https://mydreamworld:443/tmp/test.php?q=100#1 ``` w URL 完整的格式為: ``` [協定類型]://[存取資源需要的憑證資訊]@[伺服器位址]:[埠號]/[檔案路徑][檔名]?[查詢]#[片段] ``` 多出來的這一段 **存取資源需要的憑證資訊@** 格式為 帳號:密碼 若需要進行帳號密碼驗證的網頁,過去有些網站是以這樣的方式進行登入。 ``` [協定類型]://[帳號:密碼]@[伺服器位址]:[埠號]/[檔案路徑][檔名]?[查詢]#[片段] ``` ### 百分號編碼 (Percent-encoding) 百分號編碼,又稱URL encoding,是一種讓URL可以輕易識別網業資源的方法。這種編碼方式將字元分為 **保留字** 與 **未保留但合法的字元** 以及 **其餘非法字元**。 - 保留字元,這裡的字元都有特殊的涵義,包含 !、*、'、(, )、;、:、@、&、=、+、$、,、/、?、#、[? ]、[, ] - 非保留但合法的字元,例如: 英文字母、數字、-、.、_、~ - 其餘非法字元,其他非法的字元會被編碼成16進位的表示方式,並且前面加上一個%。例如:【空白】會被編碼成【%20】 - 其餘的非法字元可以透過 ASCII table 將每個字元對應到一個十六進位值轉換,而中文也是相同的,每個中文字都可以拆成三個字元,不過需要使用到 Extended ASCII table (EASCII table)。舉例來說,【中】會被轉換為【%E4%B8%AD】。 ### 了解網頁文件的傳輸協定 - HTTP #### GET 方法 GET可以想成是要向網頁查詢資料,而查詢的方式是透過告知伺服器這筆資料的一些相關參數資訊與查詢的內容會顯示在URL上,其格式如下: ``` 網址?參數資訊1=內容1&參數資訊2=內容2 ``` 可以注意到 GET 使用的幾個特點: (1) GET 是在網址後加上一個 ? 在街上參數資訊。 (2) 每個資訊會用參數 資訊 = 內容 的格式表示。 (3) 當我們有多筆參數要提供時,中間用一個&串接。 舉例來說,針對域名 mydreamworld.online 的首頁,送出參數名稱為 key 內容為 vaule 的請求: ``` https://mydreamworld.online/?key=vaule ``` ### Cookie & Session HTTP 協定是無狀態的通訊協定,也就是不會記錄過去、現在的請求與回應封包。但是為甚麼現在許多的網站都能記住我們的帳號,在下次瀏覽時也能維持登入狀態呢?這就是使用了 **Cookie** 以及 **Session** 的機制來達成的! Cookie 以及 Session 可以記錄過去成功驗證的資訊,Cookie儲存在使用者的本地端,而Session ID儲存在伺服器端。當字訓被成功驗證後,伺服器會新增一筆Session ID,並且在回傳的封包 Header 當中會加上 Set-Cookie,讓使用者的瀏覽器設置一個與Session ID相對應的Cookie。往後當你瀏覽該網站時,瀏覽器就會將Cookie的資訊也一併傳送過去,伺服器如果尋找到相對應的Session ID就可以知道目前是哪一位使用者要瀏覽網站。 ### 了解網頁伺服器 前面提到使用者如何透過瀏覽器傳送 HTTP 請求到伺服器,而這裡所說的伺服器正是網頁伺服器(Web server)。網頁伺服器儲存了網頁相關的檔案,包含了 HTML、CSS、JS 等等,在接收到用戶發出的請求後,網頁伺服器需要作出相對應的回覆,這就是網頁伺服器最主要的功能。 舉例來說,當使用者向 www.example.com/index.html 發出請求,以 Apache Sever 來說,網頁相關的檔案預設都放在 /var/www/html/ 當中,那麼網頁伺服器就要對應到 /var/www/html/index.html 並將結果回傳。 常見的網頁伺服器有 Apache、Nginx、以及Microsoft IIS。 ### 了解應用程式伺服器 應用程式伺服器是一種軟體的框架,提供一個應用程式執行的環境。有了應用程式伺服器就可以透過與資料庫互動讓網頁更加動態,除此之外,近年來的應用程式伺服器也有負載平衡(Load balancing)、故障轉移(Failover)等等的功能,讓開發者可以更加專注在設計商業邏輯。 當使用者像伺服器端發送請求後,資料經過應用程式伺服器像資料庫互動以及商業邏輯,最後將處理後的資料經由網頁伺服器回傳,就可以有更加動態的網頁了! 常見的應用程式伺服器有WebLogic、Apache Tomcat、Websphere。 ### LAB: 利用 curl 練習 curl 常用參數: | 參數 | 說明 | | -X/--request [HTTP Method] | 使用指定的 HTTP Method 傳輸 | | -i/--include | 顯示Response Header | | -H/--header [Header] | 攜帶指定的 Header 傳輸 | | -b/--cookie [Cookie 參數] | 攜帶指定的 Cookie 傳輸 | | -d/--data | 攜帶 HTTP POST Data | 練習平台: https://httpbin.org #### HTTP GET 本機測試(Docker): ``` $docker run -p 80:80 kennethreitz/httpbin ``` **請求指令** ``` $curl -X GET "https://0.0.0.0:80/get" -H "accept: application/json" ``` **回傳封包** ```json= { "args": {}, "headers": { "Accept": "*/*", "Host": "0.0.0.0", "User-Agent": "curl/7.81.0" }, "origin": "172.17.0.1", "url": "http://0.0.0.0/get" } ``` ![](https://i.imgur.com/BEu5J80.png) **請求指令** 加上參數 -i 看看 Response Header ``` $curl -i -X GET "https://httpbin.org/get" -H "accept: application/json" ``` **回傳的 Response Header** ```json= HTTP/1.1 200 OK Server: gunicorn/19.9.0 Date: Sun, 20 Nov 2022 02:37:17 GMT Connection: keep-alive Content-Type: application/json Content-Length: 188 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true ``` #### HTTP POST **請求指令** ``` $curl -X POST "http://0.0.0.0:80/post" -H "accept: application/json" ``` **回傳封包 (body)** ```json= { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "application/json", "Host": "0.0.0.0", "User-Agent": "curl/7.81.0" }, "json": null, "origin": "172.17.0.1", "url": "http://0.0.0.0/post" } ``` **請求指令** 可以加上參數 -i 看看 Response Header ``` $curl -i -X POST "http://0.0.0.0:80/post" -H "accpet: application/json" ``` **回傳的 Response Header** ```json= HTTP/1.1 200 OK Server: gunicorn/19.9.0 Date: Sun, 20 Nov 2022 02:41:51 GMT Connection: keep-alive Content-Type: application/json Content-Length: 387 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true ``` POST可以再透過 -d 參數攜帶資料 ```json= { "args": {}, "data": "", "files": {}, "form": { "test": "testdata" }, "headers": { "Accept": "*/*", "Accpet": "application/json", "Content-Length": "13", "Content-Type": "application/x-www-form-urlencoded", "Host": "0.0.0.0", "User-Agent": "curl/7.81.0" }, "json": null, "origin": "172.17.0.1", "url": "http://0.0.0.0/post" } ``` #### HTTP PUT **請求指令** ``` $curl -X PUT "http://0.0.0.0:80/put" -H "accept: application/json" ``` **回傳封包(body)** ```json= { "args": {}, "data": "", "files": {}, "form": {}, "headers": { "Accept": "application/json", "Host": "0.0.0.0", "User-Agent": "curl/7.81.0" }, "json": null, "origin": "172.17.0.1", "url": "http://0.0.0.0/put" } ``` **請求指令** 可以加上參數 -i 看看 Response Header ``` $curl -i -X PUT "http://0.0.0.0:80/put" -H "accept: application/json" ``` **回傳的 Response Header** ```json= HTTP/1.1 200 OK Server: gunicorn/19.9.0 Date: Sun, 20 Nov 2022 02:47:50 GMT Connection: keep-alive Content-Type: application/json Content-Length: 251 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true ``` ## PHP 與 MySQL ### 環境建構 建立 Dockerfile。Dockerfile 可以幫助我們輕鬆把容器跑起來 建立 docker-compose.yml。我們總共會建立3個服務,分別是 www、db、phpmyadmin。 docker-compose.yml ```json= version: "2" services: www: build: . ports: - "8080:80" volumes: - ./www:/var/www/html/ links: - db networks: - default db: image: mysql:5.7.13 ports: - "8081:3306" environment: MYSQL_DATABASE: myDb MYSQL_USER: user MYSQL_PASSWORD: test MYSQL_ROOT_PASSWORD: test volumes: - ./mysql:/docker-entrypoint-initdb.py networks: - default phpmyadmin: image: phpmyadmin/phpmyadmin links: - db:db ports: - 8082:80 environment: MYSQL_USER: user MYSQL_PASSWORD: test MYSQL_ROOT_PASSWORD: test ``` 確認檔案建立好之後,輸入指令: ``` $docker-compose up -d ``` 現在我們測試一下是否安裝完成。在www資料夾底下新增一個 index.php ```php= <?php phpinfo(); ?> ``` ![](https://i.imgur.com/0koCDc2.png) 下一次開啟直接輸入 `docker-compose up -d` 即可 也可以透過下面指令來確認哪些服務是開啟的 ``` $docker-compose ps ``` 只要 State 的部分顯示 Up 就表示正常運行中。 ![](https://i.imgur.com/nlgDx5P.png) ### 基本的輸出指令 ```php= <?php echo "HelloWorld"; ``` 首先看到第一行,<?php。PHP的程式碼必須要被放在<?php ?>之中,不過你也許會發現到前面並沒有寫到 ?>,其實這是官方的建議。如果你的檔案當中只有PHP的程式碼,而沒有其他html語法,為了避免在 ?> 後面誤加了空白、換行的等等符號,造成PHP產生錯誤,因此建議在寫純 PHP 時不要加上 ?> 符號。 ### 變數 ```php= $num = 1; echo $num; ``` 在PHP當中還有稱作可變變數的神奇功能,透過下面的範例來了解一下。 ```php= $number = 100; $var = "number"; echo ${$var}; ``` 因為 $var 的值是number,將其替換後, ${$var} 就變成了 $number,最後就會對應 $number 的值 100。 在PHP當中直接將GET到的input輸出出 index.php ```php= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Title</title> </head> <body> <form action="form.php" method="GET"> <input type="text" name="input"> <br> <input type="submit" value="submit"> </form> </body> </html> ``` form.php ```php= <?php echo $_GET["input"]; ``` ![](https://i.imgur.com/Rg3oyD1.png) ![](https://i.imgur.com/S0n5FJM.png) ### 資料型態 #### boolean boolean 也就是布林值,只會儲存 true 和 false 兩種值。在 PHP 當中如果要讓某個變數為指定的資料型態,就需要強制轉型。如果要將變數強制轉型為 boolean 型態,有下列三種方式。 ```php= boolval($變數名稱); (bool)$變數名稱; (boolean)$變數名稱; ``` #### integer 整數可以使用十進位、八進位、十六進位,也可以使用正負值。如果要使用八進位,數字前面須加上0,如果要使用十六進位,數字前面要加上一個0x,PHP預設會使用十進位; ```PHP= echo 11; // 輸出11 echo 011; // 輸出9 echo 0x11 // 輸出17 ``` 如果要將變數轉型為 intege,有三種方式: ```php= intval($變數名稱); (int)$變數名稱; (integer)$變數名稱; ``` 強制轉型整數的規則大致上是: - boolean 型態 false 轉為 0; true 轉為 1 - float 型態等同於無條件捨去小數點以下的位數 #### float 浮點數 float,也就是所謂的小數,有兩種表示的方式。 - 數字表示 - 科學記號表示 科學記號表示,也就是用 E 或是 e 來表示以 10 為底的指數,後面上數字表示次方: ```php= $number = 1500; $number = 1.5E3; $number = 1.5e3; ``` 如果要強制將變數轉型為float有四種表示方式: ```php= floatval($變數名稱); (float)$變數名稱; (double)$變數名稱; (real)$變數名稱; ``` #### string 在 PHP 當中看到幾個特定符號包起來的值就會被看作是字串,分別是: 1. 單引號 ('') 2. 雙引號 ("") 3. nowdoc 4. herdoc - 單引號,如果字串是以單引號包起來的,那麼裡面的變數以及特殊自原是不會被解析的,只會輸出純文字 ```php= $name = "Bob"; echo '{$name}' // 輸出 {$name} ``` - 雙引號,如果字串是由雙引號包起來的,裡面的便是以及特殊字元是會被解析的 ```php! "$變數名稱" "${變數名稱}" "{$變數名稱}" ``` - Nowdoc,Nowdoc是對應到單引號字串的用法,但是可以一次寫多行。Nowdoc 使用的方式是在 <<< 符號的後面,接上一個用單引號包起來的自訂標籤,最後再以自訂標籤作結束。 ```php= $var = 123; $content = <<<'tag'; 字串內容 可以寫多行 變數 {$var} 不會被解析 tag; echo $content; ``` 輸出的結果為: ``` 字串內容 可以寫多行 變數 {$var} 不會被解析 ``` - Heredoc,Heredoc 是對應到雙引號字串的用法,但是可以一次寫多行。Heredoc使用的方式是在 <<< 符號的後面接上一個自訂的標籤 (可以用雙引號包起來),最後在以自訂標籤作結束。 ```php= $var = 123; $content = <<< "tag" 字串內容 可以寫多行 變數 {$var} 會被解析 tag; echo $content; ``` 輸出的結果為: ``` 字串內容 可以寫多行 變數 123 會被解析 ``` #### array 陣列可以用來儲存許多元素 (elemet),透過索引 (key) 可以取得相對應元素的值 (vaule)。 宣告陣列的方式有兩種,分別是指定 key 與沒有指定 key。指定 key 的寫法有兩種: ```php= $var = array( key1 => vaule1, key2 => vaule2, ... ); $var = [key1 => vaule1, key2 => vaule2, ...] ``` 當我們要輸出其中一個元素的值,就可以這麼寫: ```php= echo $var[key1]; ``` 沒有指定 key 的寫法: ```php= $var = array(vaule1,vaule2, ...); ``` 在沒有指定 key 的情況下,預設會從數字0開始依序遞增,因此如果我們要輸出 var 2; 當中最前面的元素,可以這麼寫: ```php= echo $var[0]; ``` 如果要刪除陣列中的某個元素,可以使用unset() 函數。例如要刪除 var 陣列中 key1 所對應的元素: ```php= unset($var[key1]); ``` #### 執行運算子 如果 php.ini 當中 safe_mode 是關閉的情況下,將系統指令包在 ‵‵ 符號之間,會回傳執行結果。效果等同於 shell_exec() 函數。 ```php= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Title</title> </head> <body> <?php echo `ls`; ?> <br><br> <form action="form.php" method="GET"> <input type="text" name="input"> <br> <input type="submit" value="submit"> </form> </body> </html> ``` ![](https://i.imgur.com/CeMl5AR.png) ### MySQL 資料庫 (Database) 是儲存資料的地方,通常會與資料庫管理系統 (Database Management System,)一起使用,透過DBMS可以和資料庫進行互動,包含了查詢、新增、刪除、更新等等操作。 資料庫的架構由上至下依序為 資料庫 => 資料表 => 資料欄位 => 資料 #### SELECT 查詢 當我們要在資料庫中查詢資料時,就必須要指定查詢的資料表以期要查詢的欄位: ```php= SELECT [欄位名稱] FROM [資料表名稱]; SELECT 商品名稱 FROM 商品資料; ``` 如果想要一次查詢同一個資料表的多個欄位,可以直接上所有要查詢的欄位: ```PHP= SELECT [欄位名稱1], [欄位名稱2], ... FROM [資料表名稱]; SELECT 姓名, 手機號碼 FROM 客戶名稱; ``` 另外,如果要查詢一張資料表內的所有欄位,可以用 * 取代欄位名稱: ```PHP= SELECT * FROM [資料表名稱]; ``` 有時候我們只需要篩選出符合特定條件的資料,這時候就可以再加上 WHERE 來指定條件: ```PHP= SELECT [欄位名稱] FROM [資料表名稱] WHERE [條件]; ``` 如果要跨資料表查詢的話,有兩種常用的方式,分別為 JOIN 以及 UNION ### LAB: 透過 Docker 練習 PHP 與 MySQL 必備知識 - Docker 與 docker-compose 安裝 - PHP 與 SQL - docker-compose 指定 在這裡我們總共需要建立三個服務: 1. php-apache-enviroment,這裡是PHP apache 的服務 2. db,這裡是MySQL的服務 3. phpmyadmin,這裡是 phpmyadmin 的服務 #### Step1 建立 PHP 環境 docker-compose.yml ```json= version: "3.8" services: php-apache-environment: container_name: php-apche build: context: . dockerfile: Dockerfile depends_on: - db volumes: - ./php/src:/var/www/html/ ports: - 8000:80 db: container_name: db image: mysql restart: always ports: - "9906:3306" environment: MYSQL_DATABASE: MYSQL_DATABASE MYSQL_USER: MYSQL_USER MYSQL_PASSWORD: MYSQL_PASSWORD MYSQL_ROOT_PASSWORD: MYSQL_ROOT_PASSWORD phpmyadmin: image: phpmyadmin/phpmyadmin restart: always ports: - 8082:80 environment: PMA_HOST: db depends_on: - db ``` 接著我們要到 php/src/ 底下新增一個index.php: ```php= <? phpinfo(); ``` 重新整理後就會看到以下的畫面 ![](https://i.imgur.com/3XJpPQp.png) #### 建立 MySQL 環境 我們的 docker-compose.yml 當中需要包含幾個資訊: - container 名稱 ```php= container_name: db ``` - container image,這裡使用 dockerhub 的官方 image mysql ```php= image: mysql ``` - restart,這裡特別設定一個 restart,是為了在設定檔有更新時可以自動重新啟動服務,因此這裡設定 always ```php= restart: always ``` - environment,在這裡我們針對 MySQL 的帳號密碼與資料庫進行設定。這裡的密碼與名稱都可以自訂。 ```php= enviroment: MYSQL_ROOT_PASSWORD: MYSQL_ROOT_PASSWORD MYSQL_DATABASE: MY_DATABASE MYSQL_USER: MYSQL_USER MYSQL_PASSWORD: MYSQL_PASSWORD ``` - ports,這裡要設定本機與 container 所對應的 port 。例如我們將本機的 9906 port 對應到 container 的 3306 port ``` ports: - 9906:3306 ``` 最後要回去前面的 php 當中加上 depends_on,把我們剛剛新增的 db 新增進去 ``` depends_on: - db ``` 接下來為了方便在 PHP 當中操作 MySQL,我們需要另外下載 mysqli 擴充套件。在資料夾當中新增一個 Dockerfile,內容如下: ``` FROM php:8.0-apache RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli RUN apt-get update && apt-get upgrade -y ``` 接著我們可以修改 index.php 來測試 MySQL 與 PHP 的連線是否正常 ```php= <? $host = 'db'; $user = 'MYSQL_USER'; $pass = 'MYSQL_PASSWORD'; $conn = new mysqli($host,$user,$pass); if ($conn->connect_error) { die("Connection failed " . $conn->connect_error); } else { echo "Connected to MySQL server successfully!"; } ?> ``` 一開始我遇到這個問題 ![](https://i.imgur.com/3wBiS0P.png) 後面發現是因為我沒有去 bulid 所以 Dockerfile 沒跑到 ``` docker-compose up --bulid ``` 輸入後就成功解決了! ![](https://i.imgur.com/bH2dv35.png) 接下來來到phpmyadmin進入到 SQL 的選項當中,輸入下列SQL指令: ```sql= drop table if exists `users`; create table `users` ( id int not null auto_increment, username text not null, password text not null, primary key (id) ); insert into `users` (username,password) VALUES ("admin","password"), ("Alice","this is my password"), ("Job","12345678"); ``` ![](https://i.imgur.com/qVFn0wi.png) 接下來就可以在左側看到剛剛新增的 table users。進入到 users table 後可以看到剛剛新增的三筆數據 ![](https://i.imgur.com/OcRznTG.png) 最後再回到 index.php 嘗試看看SQL查詢: ```php= <?php $host = 'db'; $user = 'MYSQL_USER'; $pass = 'MYSQL_PASSWORD'; $mydatabase = 'MYSQL_DATABASE'; $conn = new mysqli($host,$user,$pass,$mydatabase); $sql = 'SELECT * FROM users'; if ($result = $conn->query($sql)) { while ($data = $result->fetch_object()) { $users[] = $data; } } foreach ($users as $user) { echo "<br>"; echo $user->username . " " . $user->password; echo "<br>"; } ``` ![](https://i.imgur.com/DWlwspa.png) ### SQL Injection | SELECT * | FROM products | WHERE category = 'Gifts' | AND released = 1 | | ---------------- | -------------------- |:------------------------ | ---------------- | | 選擇所有欄位名稱 | 從資料表products當中 | 找到category是Gifts | 並且released是1的資料 | | SELECT * | FROM users | WHERE username = 'admin' | AND password = '123' | | -------- | -------- | -------- | ------- | | 選擇所有欄位名稱 | 從資料表users當中 | 找到username是admin | 並且123是password的資料 | #### 自架靶機練習 SQL Injection 環境建構: Dockerfile ```php= FROM php:8.1-apache RUN docker-php-ext-install mysqli ``` docker-compose.yml ```php= version: "3.8" services: www: build: . ports: - "8080:80" volumes: - ./www:/var/www/html/ - ./config/custom.ini:/user/local/etc/php/conf.d/custom.ini links: - db networks: - default db: image: mysql ports: - "8081:3306" environment: MYSQL_DATABASE: myDb MYSQL_USER: user MYSQL_PASSWORD: test MYSQL_ROOT_PASSWORD: test volumes: - ./mysql:/docker-entrypoint-initdb.d networks: - default phpmyadmin: image: phpmyadmin/phpmyadmin links: - db:db ports: - "8082:80" environment: MYSQL_USER: user MYSQL_PASSWORD: test MYSQL_ROOT_PASSWORD: test ``` 新增資料夾 mysql 存放 sql 檔案,可預先撰寫 SQL 於檔案中。 在 mysql 資料夾新增 db.sql: ```sql= SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; SET time_zone = "+08:00"; CREATE TABLE `users` ( `id` int(11) NOT NULL auto_increment, `username` varchar(64) NOT NULL, `password` varchar(64) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `users` (`id`, `username`, `password`) VALUES ("1", "YouKlike" , "rjeaoisrjaeoisr34131489dsa"); INSERT INTO `users` (`id`, `username`, `password`) VALUES ("2", "jeff" , "fejasifjesoiafjeoisafjeoispz") ``` 在 www 資料夾,新增 config.php 檔案,檔案內容如下: ```php= <?php define('DB_SERVER' , 'db'); define('DB_USERNAME', 'root'); define('DB_PASSWORD', 'test'); define('DB_NAME', 'myDb'); $link = mysqli_connect(DB_SERVER,DB_USERNAME,DB_PASSWORD,DB_NAME); if ($link === false) { die("ERROR: Could not connect." . mysqli_connect_error()); } ``` 在 www 資料夾,新增 sqli.php 檔案,檔案內容如下: ```php= <form method="POST" action=""> <input id="username" placeholder="Username" required="" autofocus="" type="text" name="username"> <input id="password" placeholder="Password" required="" type="password" name="password"> <button type="submit"> 登入 </button> </form> <?php $flag = "You Got SQL Injection."; if (isset($_POST['username']) && isset($_POST['password'])) { $username = $_POST['username']; $password = sha1($_POST['password']); require_once('config.php'); $sql = "SELECT * FROM `users` WHERE `username` = '$username' and `password` = '$password';"; if ($result = mysqli_query($link, $sql)) { if ($row = mysqli_fetch_array($result)) { echo $flag; } else { echo "登入失敗"; } } } ?> ```