본문 바로가기

C#/근웹 연대기

c#으로 근본 없는 웹서버 개발기 8 : sessionID & 소켓연결버그

⚠WARNING
ASP.NET에 대한 포스팅이 아닙니다
나가실 문은 오른쪽 하단입니다

 

 

한번 접속하고 두 번 접속한다면, 그 녀석이 그놈이었는지 대한 정보가 필요하기 마련이다.

http통신은 항상 필요할 때만 접속을 하기 때문에 브라우저는 내가 그놈이었다는 정보가 필요하기 마련이다.

그래서 쿠키값으로 일정한 값을 가지고 있으면서 내가 그놈이 이었다고 접속할 때마다 식별을 당하기를 원한다.

이름은 서버마다 다른 취향이지만 값의 형태는 엇비슷하다.

이것이 쉽게 노출될 때, 저 값을 복사될 때, 로그인을 빼앗기는 현상이 발생할 수 있으므로 공용 컴퓨타를 사용할 경우는 꼭 로그아웃을 하고, 그것도 불안하면 웹브라우저를 완전히 닫고 또 그것도 불안하면 브라우저 설정에서 쿠키 캐시를 일괄 삭제하여야 한다. 그리고 https를 사용하지 않는 사이트는, 되도록이면 로그인 같은 개인정보를 오가는 것은 중간에 스니핑 된다면 완전 홀가 벗은 상태로 보여주는 꼴이 된다. 그러므로 https를 사용하지 않는 사이트는 브라우저가 심한 경고를 하기 마련인데 굳이 들어가면 안 좋은 꼴이 날 확률이 있다.

 

쿠키값을 파싱, 그리고 sessionID를 생성하는 로직을 다음과 같이 작성하였다.

public static Dictionary<string, string> getCookie(string s)
{
    var dic = s.Split(';').Select(x => x.Split('='))
    .ToDictionary(x => x[0].Trim(), x => x.LastOrDefault()?.Trim());
    return dic;
}
public static string newSessionID(Socket client)
{
    int seed = (int)(DateTime.UtcNow.Ticks % int.MaxValue);
    var intNx = new Random(seed).Next();
    var id = CreateMD5(client.GetHashCode() + "^_^" + intNx);
    return id;
}

static System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();
static string CreateMD5(string input)
{
    byte[] inputBytes = Encoding.UTF8.GetBytes(input);
    byte[] hashBytes = md5.ComputeHash(inputBytes);
    return String.Join(null, hashBytes.Select(x => x.ToString("x2")));
}

대부분의 사이트들의 sessionID에 해당하는 값의 모양새가 MD5를 이용하는 느낌이 강해서 그걸로 하기로 했다.

MD5는 한 글자만 바뀌어도 전체적으로 확 바뀌는 것이 이런 곳이 이용당하기 좋다.

로직을 짤 때 주의할 점은 절대로 중복이 돼서는 안 된다는 것이다. 항상 고유한 값이 되어야 한다.

 

그리고 위의 코드를 이용하는 부분.

static void response(Socket client, Dictionary<string, string> dic)
{
    string publicPath = Path.Combine(Util.sPath, "public");
    var ftag = Util.getFInfo(publicPath + dic["url"]);
    var info = ftag.info;
    if (!info.Exists || !info.FullName.StartsWith(publicPath))
    {
        resForbadrequest(client, "404 Not found");
        return;
    }

    StringBuilder sb = new StringBuilder(100);
    bool m = ftag.etag == dic.GetValueOrDefault("If-None-Match");
    var cookie = Util.getCookie(dic.GetValueOrDefault("Cookie") + "");

    sb.AppendLine($"HTTP/1.1 {(m ? "304 Not Modified" : "200 ok")}");
    sb.AppendLine("Accept-Ranges: none");
    sb.AppendLine("Cache-Control: public, max-age=0");
    sb.AppendLine("Last-Modified: " + ftag.modiTime);
    sb.AppendLine("Etag: " + ftag.etag);
    sb.AppendLine("Date: " + Util.uTime);
    sb.AppendLine("Content-type: " + ftag.minetype);
    sb.AppendLine("Server: test server");
    sb.AppendLine("Content-Length: " + info.Length);
    sb.AppendLine("Connection: close");
    if (cookie.ContainsKey("sessionID") == false)
    {
        sb.AppendLine("set-cookie: sessionID=" + Util.newSessionID(client) + "; path=/; httponly");
    }
    sb.AppendLine();

    client.Send(Encoding.UTF8.GetBytes(sb.ToString()));
    if (!m) client.Send(File.ReadAllBytes(info.FullName));
}

로직요약 : 쿠기값에 sessionID 라는 키 값이 없으면 새로 할당한다.

path=/는 루트 이하의 경로에서도 쿠키를 사용할 수 있음을 뜻한다.

httponly는 보안을 위한 것으로 자바스크립트로 document.cookie의 값에 등록되지 않게 한다.

그런데, 사용 안 한 사이트도 있는 걸 보니, 쓰임새는 낮아 보인다. 

그리고 브라우저에서는 허용을 안 하지만 혹시나 ../ 을 이용해서 루트 폴더로 접근할 수 있는 가능성을 갑자기 감지하여 IF문과 StartWith로 방지책을 마련했다. 

그렇다.

항상 보안에 대해서 고민하는 습관이 더욱 시큐어 한 코딩을 가능케 한다.

그렇다.

코드는 프리큐어 하더라도 보안만큼은 시큐어 해야 한다.

Yes! secure~

 

참고로 세션 아이디가 잘 할당되는지 테스트를 하다가 크리티컬 한 버그를 발견하였는데,

브라우저의 새 탭으로 연속적으로 접속을 하면 문제가 없으나 다른 브라우저에서도 접속할 경우, 새로운 클라이언트 소켓을 할당하지 않고 대기 타고 있다가 먼저 한 브라우저가 새로고침을 하면 그때서야 소켓이 할당이 되고 작동을 한다는 점이다. 여기서 더 나아가 휴대폰으로 접속을 하면 그 다른 브라우저가 새로고침을 하지 않으면 먹통이 되어 캐스케이딩 병목현상이 발생한다는 것이다.

로직상으로는 소켓을 close 하면 연결고리가 끊어 저서 처음의 상태가 되어야 하는데, 실상은 처음 접속당한 클라이언트에 저당이라도 잡힌 듯이 꼼작을 못하는 상태가 된다.

즉, 그 스레드 자체는 임무를 끝내고 처음의 상태로 돌아가야 하는데, 먼가 가 발복을 잡는다.

이것을 간단히 해결하기 위해서는 자식 스레드를 할당하여 작업을 시켜면 되는데 , 근본적인 문제의 해결책에는 다다르지 못한다. (아마 스레드가 죽지 않고 쌓일 것이다.)

그렇다

문제의 회피는 해결이 아닌 것이다.

그렇다

이것이 어떤 대형 서비스였다면 결국 이러한 작은 코막힘이 나중에 접속자가 커져 숨 막힘이 되어 질식死다.

그렇다

이것을 해결 하지 않고는 더 이상 진행 불가능한 것이다.

つづく