MySQL
MySQL은 Apache 재단에서 만든 대표적인 오픈소스 DBMS이다.
PHP - MySQL 연동
PHP는 대표적인 백엔드 언어로, 당연하게도 MySQL과 연동이 가능하다.
PHP와 MySQL을 연동하는 방법은 크게 두 가지가 있다. mysqli를 이용하는 방법, POD를 이용하는 방법이 있다. 각각에는 장단점이 있다.
PDO는 MySQL이 아닌 다른 DBMS와도 연동이 가능하다는 점에서 확장성이라는 장점을 가지고 있지만, MySQL의 최신 기능을 전부 지원해주지 못한다. 예를 들어, 여러 개의 query를 한 번에 보내는 Multiple Statements를 지원하지 않는다. 반면 mysqli에서는 Multiple Statements와 같은 MySQL의 다양한 기능들을 제공한다.
※ PHP 내장 라이브러리 중, mysql(mysqli 아님)이라는 라이브러리도 존재하지만 PHP 5.5.0 버전부터 사용을 권장하지 않고, 7.0.0에서부터는 삭제되었다.
◇ mysqli를 이용한 연동
mysqli는 php에 내장된 기능으로, MySQL Improved Extension의 줄임말이다. MySQL와의 연결에 최적화된 확장 라이브러리다.
C언어처럼 절차지향형 문법으로도 사용 가능하고, Java와 같이 객체지향적으로도 사용이 가능하다.
mysqli: procedural style
먼저 절차지향형 사용법을 소개하겠다.
id | name | hobby | age |
1 | David | Playing a game | 19 |
2 | Jay | Reading books | 23 |
3 | Kevin | Watching anime | 22 |
예제로 사용할 데이터, `my_table`이다.
<?php
define('DB_SERVER', 'localhost'); // Make texts into constants
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'db_name');
$db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$query = "SELECT * FROM my_table";
$query_result = mysqli_query($db_conn, $query);
while($result = mysqli_fetch_array($query_result)) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
mysqli: object-oriented style
PHP는 OOP(Object-Oriented Programming)을 지원한다. mysqli 또한 이에 맞게 작성된 라이브러리로, OOP 스타일로도 코딩이 가능하다.
<?php
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'db_name');
$mysql = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$query = "SELECT * FROM my_table";
$query_result = $mysql->query($query);
while($result = $query_result->fetch_array()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
결과는 절차지향 프로그래밍으로 했을 때와 똑같다.
◆ PDO를 이용한 연동
PDO 또한 PHP에 내장된 라이브러리로, PHP Data Object의 줄임말이다. PDO는 mysqli와는 다르게 절차지향 프로그래밍이 불가능하고, 객체지향 프로그래밍만 가능하다.
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM my_table";
$query_result = $pdo->query($query);
while($result = $query_result->fetch()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
mysqli에서 객체를 만들때와 다른 점이 있다. PDO에서는 `constructor`가 받아들이는 파라미터가 약간 다르다.
mysqli에서는 `$hostname`, `$username`, `$password`, `$database`를 파라미터로 두는 반면, PDO에서는 기본적으로 `$dsn`, `$username`, `$password`를 받는다.
`$username`, `$password`는 공통으로 받는 것을 확인할 수 있는데, PDO에서는 `$dsn`이란 것이 존재한다. 이 `$dsn`은 무슨 문자열일까?
DSN
DSN은 Data Source Name의 줄임말이다. PHP 공식 문서에 따르면,
The Data Source Name, or DSN, contains the information required to connect to the database.
즉, 데이터베이스에 연결하기 위한 정보들을 담는 문자열인 것이다. PDO_MYSQL DSN 공식 문서를 참고하면 이 `$dsn` 자리에 어떠한 문자열이 와야하는지 알 수 있다.
`"[DSN prefix]:[host];[port];[dbname];[unix_socket];[charset]"`
- `[DSN prefix]`: MySQL은 이 자리에 `mysql`을 사용하면 된다.
- `[host]`: 연결할 MySQL이 있는 IP 주소를 쓰면 된다. 만약 자신의 컴퓨터라면 `localhost`로 대체 가능하다.
- `[port]`: 연결할 IP 중에서도 포트 값이다. 이 필드를 사용하지 않는다면 기본적으로 MySQL의 기본 포트인 3306으로 시도할 것이다.
- `[db_name]`: 사용할 DB의 이름이다. 이 필드를 사용하지 않고 "USE [db_name]" 쿼리를 날리는 것도 가능하다.
- `[unix_socket]`: Unix Domain Socket을 사용할 때 쓰는 필드다. Unix Domain Socket은 다른 컴퓨터와의 통신이 아닌, 같은 컴퓨터 내의 다른 프로세스 간의 통신을 위한 IPC(Inter-Process Communication) 기법을 위한 소켓이다. 그러니 당연하게도 이 필드를 사용한다면 `[host]`, `[port]`와 같이 쓸 수 없다. 이 필드의 값으로는 .sock 파일의 경로를 작성하면 된다. (ex. unix_socket=/tmp/mysql.sock)
- `[charset]`: 문자열 인코딩 방식을 지정해줄 수 있다.
기본적으로 필드들은 세미콜론 `;`을 통해 구분을 짓고, 맨 마지막에는 쓰지 않아도 된다. 다만, `[DSN prefix]` 뒤에는 세미콜론이 아닌 콜론 `:`이 온다는 점을 주의하자.
PDO를 이용한 코드
위에 설명한 DSN을 제외한 나머지 코드는 mysqli와 크게 다를 게 없다.
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM my_table";
$query_result = $pdo->query($query);
while($result = $query_result->fetch()) {
echo "<p>Name is {$result['name']}.
He(or She) likes {$result['hobby']}
and is {$result['age']} years old.</p>";
}
?>
SQL Injection
PHP에서 쿼리를 그대로 사용하는 것은 상당히 위험하다. SQL Injection이라는 공격 기법에 취약하기 때문이다.
// login_proc.php
<?php
define('DB_SERVER', 'localhost');
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
define('DB_NAME', 'loginDB');
function check_login($id, $pw) {
$db_conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$login_query = "SELECT * FROM user_info WHERE name='{$id}' and password='{$pw}";
$db_conn->query($db_conn, $login_query);
return mysqli_fetch_array($result)['id'];
}
?>
예를 들어 위와 같은 로그인 검사 코드가 있다고 하자. 얼핏 보기엔 문제가 없어보이지만, `$login_query`에 상당히 위험한 문제가 있다.
check_login("my_id", "password1234");
보통이라면 사용자의 입력을 받아 위와 같이 함수가 실행될 것이다. 하지만 SQL Injection을 알고 있는 사람은 위와는 다르게 함수를 호출할지도 모른다.
check_login("' or '1'='1", "' or '1'='1");
함수가 이런 파라미터를 갖고 실행된다면, `$login_query`의 값이 다음과 같이 정해진다.
" SELECT * FROM user_info WHERE name='' or '1'='1' and password='' or '1'='1' "
조건문을 차례로 해석하면 다음과 같다.
- `name`이 빈 문자열이거나 '1'이 '1'과 같다 → 항상 True
- `password`가 빈 문자열이 '1'이 '1'과 같다 → 항상 True
아이디와 패스워드 없이 항상 로그인에 성공을 시킬 수 있는 것이다. 이는 로그인 페이지를 완전히 무력화시킬 수 있는 공격 기법으로, 매우 위험하다.
Prepared Statements
SQL Injection은 오래된 공격 기법이고, 당연하게도 막는 방법이 존재한다. 그 중에서도 아주 간단한 방법은 Prepared Statements를 사용하는 방법이다. 문자열이 쿼리 안으로 그대로 들어가는 것을 방지해준다. PDO, mysqli 전부 이 방법을 사용할 수 있다.
mysqli Prepared Statements
function check_login($id, $pw) {
$db_conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
$stmt = $db_conn->prepare("SELECT * FROM user_info WHERE name=? and password=?");
$stmt->bind_param("ss", $id, $pw);
$stmt->execute();
$result = $stmt->get_result();
return mysqli_fetch_array($result)['id'];
}
쿼리에 들어갈 변수의 자리를 `?`로 잠시 비워둔 문자열을 `$db_conn`의 `prepare` 함수의 파라미터로 넘긴다. 그 결과가 Prepared Statements가 되니, 변수로 잘 받아준다(`stmt`).
`?`로 빈 공간을 변수로 채워줄 차례다. `stmt`의 `bind_param` 함수를 이용하면 변수를 넣을 수 있다. 대신 첫번째 파라미터로 자료형을 지정해줄 필요가 있다. 두번째 파라미터부터의 자료형을 차례로 하나의 문자를 통해 정할 수 있다.
character | description |
s | string 자료형 |
i | int 자료형 |
d | float 자료형 |
b | 패킷에 담겨 보내질 BLOB 타입의 변수 |
사용할 자료형에 알맞는 알파벳을 순차적으로 이어붙인 문자열을 첫번째 파라미터로 넘기면 된다.
참고로 BLOB은 Binary Large OBject의 줄임말로, 이미지와 같은 큰 바이너리 파일을 저장할 때 사용되는 데이터베이스 자료형이다.
check_login("' or '1'='1", "' or '1'='1");
이제 이런 파라미터를 넣어 함수를 실행시켜도 이상을 일으키지 않음을 확인할 수 있다.
PDO Prepared Statements
<?php
define('DB_USERNAME', 'username');
define('DB_PASSWORD', 'password');
function check_login($id, $pw) {
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM user_info WHERE name=:name and password=:password";
$stmt = $pdo->prepare($query);
$stmt->bindParam("name", $id);
$stmt->bindParam("password", $pw);
$stmt->execute();
$result = $stmt->fetch();
return $result['id'];
}
?>
`prepare` 함수에 들어가는 쿼리문이 약간 다른 것을 확인할 수 있다. PDO를 사용할 때도 `?`로 빈 공간을 표시할 수 있다. 하지만 PDO는 그런 물음표 말고도 다른 방법이 존재한다. 위와 같이 콜론 `:` 다음에 단어를 쓰면 해당 키워드를 빈 공간으로 표시할 수 있다.
1. $stmt->bindParam("name", $id);
2. $stmt->bindParam(":name", $id);
3. $stmt->bindParam(1, $id);
세 가지 방식으로 변수를 대입할 수 있다.
- 아까 쓴 키워드를 첫번째 파라미터로, 변수를 두번째 파라미터로 전달하는 방법
- 키워드 앞에 콜론 `:`을 붙여서 명시한 키워드를 첫번째 파라미터로, 변수를 두번째 파라미터로 전달하는 방법
- 빈공간의 순서를 세어 첫번째 파라미터로 전달하고, 변수를 두번째 파라미터로 전달하는 방법
`bindParam()` 방식을 이용하면 값을 한 번에 하나씩밖에 전달할 수 없다.
PDO Prepared Statements 값 여러개 한 번에 전달하기
생각보다 훨씬 쉽게 한 번에 모든 값들을 전달할 수 있다.
function check_login($id, $pw) {
$pdo = new PDO("mysql:host=localhost;dbname=db_name", DB_USERNAME, DB_PASSWORD);
$query = "SELECT * FROM user_info WHERE name=? and password=?";
$stmt = $pdo->prepare($query);
$stmt->execute([$id, $pw]);
$result = $stmt->fetch();
return $result['id'];
}
키워드 대신 `?`로 대체하고, `execute`를 실행할 때 array의 형태로 값들을 담아 넘기면 된다.
'Study > with normaltic' 카테고리의 다른 글
SegFault CTF - 5주차 - 1 (0) | 2025.05.06 |
---|---|
SegFault CTF - 4주차 (0) | 2025.04.24 |
로그인 유지: 쿠키 & 세션 (0) | 2025.04.23 |
간단한 로그인 페이지 (1) | 2025.04.08 |
Web Server와 Web Application Server (0) | 2025.04.05 |