[컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

cors 설정 / redirect fetch / fetch cors

web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

  • 다음 sequence diagram 은 만약 내가 site.a.com 에 접속한 상황에서 fetch 로 api.a.com 에 request 를 했다. 그 상황에서 api.a.com 가 redirect site.a.com 를 나에게 던져준 경우이다.
  • 이 경우 예상치 못하게 cors error 가 떴다.

CORS error 가 뜨는 이유

CORS error 가 뜨는 이유는 Origin 이 null 로 set 돼서 site.a.com 로 request 를 보내기 때문이다.

  • 여기를 보면, 처음 request 를 보낸 url에서 바뀐적이 있으면, redirect-tainted origin 으로 본다. 그렇기에 Origin 이 null 로 잡힌다.
  • 이러면 origin 이 ‘null’ 로 바뀌었다고 봐야 하기 때문에 site.a.com 에서 CORS 를 지원하기 위한 설정을 잡아줘야 한다.

다음처럼 api.a.com 으로 보내고 api.a.com 으로 redirect 되는 경우는 다음처럼 CORS error 가 안뜨고, 잘 동작한다.

api.a.com, api.a.com

CORS error 가 안뜨게 수정

  • 아래는 이때 CORS error 가 뜨지 않도록 header 를 맞춰준 예시이다.

조정한 내용:

  • Accee-Control-Allow-Origin: null

  • Access-Control-Allow-Credentials: true

  • Access-Control-Allow-HeadersAccess-Control-Request-Headers 에서 요청한 header를 넣어준다.

  • Access-Control-Allow-MethodsAccess-Control-Request-Method에 맞춰서 지정

  • api.a.com 으로 보내고 site.a.com 으로 redirect 되는 경우 : mermaid link

api.a.com, site.a.com

fetch 를 사용할 때 redirect :

CORS error 가 안뜨게 하려면

CORS error 가 안뜨게 하려면, 다음처럼 설정을 하면 된다.

client :

  • `credentials : ‘omit’
  • mode : 'no-cors'

server :

  • Access-Control-Allowed-Origin: *
  • Access-Control-Allow-Credentials: false

Fetch: Cross-Origin Requests

  • client 에서 Origin을 보내면, 그것을 server가 본다.
  • server 는 그에 따라 Access-Control-Allow-Origin 을 만들어서 response 에 보낸다.
  • browser 가 trusted mediator(신뢰할 수 있는 중재자) 역할을 한다. 즉, browser가 이제 client 에서 보냈던 origin 이랑 서버가 보낸 Access-Control-Allow-Origin 값을 비교해서 값이 적절하면, javascript 가 response 값을 access 할 수 있게 해준다.

javascript 가 기본적으로 접근할 수 있는 safe header 는 다음과 같다.

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

js 가 다른 header 에 접근하고 싶어도 browser가 접근을 막는다. 다만 server 에서 Access-Control-Expose-Headers 에 추가된 header는 browser가 js 한테 열어준다.

이제는 browser가 안전하지 않은 요청(unsafe request)는 바로 날리지 않는다. preflight request 를 먼저 날리고 나서 날리게 된다.

  • Access-Control-Request-Method : 어떤 method 로 보내려는지 적는다.
  • Access-Control-Request-Headers : 보내려는 request 가 갖고 있는 header들을 적는다.
  • Origin : request 가 어디서 보내는 request 인지를 적게 된다.
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Encoding,API-Key

서버단에서 redirect 시켜서 CORS 피하기

  • preflight 등의 성능을 방해하는 요소를 줄일 수 있다.
                              ┌─►  http://localhost:8081       
   http://localhost:8080      │     ┌─────────────┐            
                              │     │             │            
   http://localhost:8080/api──┘     │   WAS       │            
     ┌─────────────┐                │             │            
     │ web server  │                │             │            
     │     ┌───────┤                │             │            
     │     │       │                └─────────────┘            
     │     │ asset │                                           
     │     │       │                                           
     └─────┴───────┘                                           

[컴][웹] Http Header Origin 이 null 인 경우

spec , origin이 널 / null 인 경우 / why origin null

Http Header Origin 이 null 인 경우

origin 값은 opaque origin 또는 tuple origin 인데, tuple origin 이 정상적인 값으로 ‘http://daum.net’ 같은 값이고, opaque origin (이해하기 힘든 origin)은 null 이라고 보면 된다.

The serialization of ("https", "xn--maraa-rta.example", null, null) is "https://xn--maraa-rta.example".

아래 링크에서 origin 이 null 이 되는 모든 case 를 이야기 해준다.

fetch 를 A에 했는데, B 로 redirect 되는 경우 Origin 은 null

fetch 를 한 경우 다른 origin 으로 redirect 하면 null 이 된다.

See Also

  1. 쿠…sal: [컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

[컴] 쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

 에러 / 장애 / 캐시 / 레디스 이슈 / 레디스 버그 /

쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

장애

  • 2019년 7월 24일 오전 7시경부터 쿠팡 판매 상품의 재고가‘0’으로 표시돼, 소비자는 관련 상품의 주문 및 구매할 수 없었다.

장애원인

쿠팡에서 직접적으로 밝히진 않았지만, 장애원인으로 지목되는 것은 다음 이슈이다.

간략하게 이야기하면 item 을 추가할 때 쓰는 dictAddRaw 함수의 버그이다. long long(64bit) 으로 사용해야 할 index를 int 로 사용해서 문제가 발생했다.

Reference

  1. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 < 게임·인터넷 < 기사본문 - 디지털투데이 (DigitalToday), 2019-07-24
  2. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 : 클리앙

[컴] Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

스프링 / springboot / 스프링부트 / security

Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

  • WebFilterChainProxy.filter()에서 WebFluxSecurityConfiguration.securityWebFilterChains 을 돌면서 실행한다.
  • WebFilterChainProxy@Bean WebFluxSecurityConfiguration.springSecurityWebFilterChainFilter 에 의해서 new 된다.
  • securityWebFilterChains@Autowire setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) 에 의해 new 된다.
  • WebFluxSecurityConfiguration@EnableWebFluxSecurity 에 의해 불려진다.
  • @EnableWebFluxSecuritySecurityConfig 에 설정돼 있다.
  • @Bean SecurityConfig.securityWebFilterChain 에서 filter를 정하고, ServerHttpSecurity.build() 를 호출하는데, 이 때 filter들이 실제로 추가된다.
    • formLogin 인 경우 login url 인 ‘/login’ 도 설정된다.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig
    @Bean
    SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            ...
            .authorizeExchange(exchange -> exchange
                    ...
                    .and()
                    .formLogin(formLogin -> formLogin
                            .authenticationSuccessHandler(authenticationSuccessHandler)
                            .authenticationFailureHandler(authenticationFailureHandler)
                    )
            );
        return http.build();    // ServerHttpSecurity.build
    }

public class ServerHttpSecurity
    private FormLoginSpec formLogin;
    public SecurityWebFilterChain build() {
        if (this.formLogin != null) {
            if (this.formLogin.authenticationManager == null) {
                this.formLogin.authenticationManager(this.authenticationManager);
            }
            if (this.formLogin.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.formLogin.securityContextRepository);
            }
            else if (this.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.securityContextRepository);
            }
            else {
                this.formLogin.securityContextRepository(new WebSessionServerSecurityContextRepository());
            }
            // add filter to the this.webFilters
            this.formLogin.configure(this);
        }
        ...
        AnnotationAwareOrderComparator.sort(this.webFilters);
        List<WebFilter> sortedWebFilters = new ArrayList<>();
        this.webFilters.forEach((f) -> {
            if (f instanceof OrderedWebFilter) {
                f = ((OrderedWebFilter) f).webFilter;
            }
            sortedWebFilters.add(f);
        });
        sortedWebFilters.add(0, new ServerWebExchangeReactorContextWebFilter());

        return new MatcherSecurityWebFilterChain(getSecurityMatcher(), sortedWebFilters);
    }

    public final class FormLoginSpec {
        protected void configure(ServerHttpSecurity http) {
            if (this.authenticationEntryPoint == null) {
                this.isEntryPointExplicit = false;
                loginPage("/login");
            }
            ...
        }
    }


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class,
        ReactiveOAuth2ClientImportSelector.class })
@Configuration
public @interface EnableWebFluxSecurity {

}

@Configuration(proxyBeanMethods = false)
class WebFluxSecurityConfiguration {

    private List<SecurityWebFilterChain> securityWebFilterChains;

    ...

    @Autowired(required = false)
    void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) {
        // SecurityConfig.securityWebFilterChain()
        this.securityWebFilterChains = securityWebFilterChains;
    }

    @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
    @Order(WEB_FILTER_CHAIN_FILTER_ORDER)
    WebFilterChainProxy springSecurityWebFilterChainFilter() {
        return new WebFilterChainProxy(getSecurityWebFilterChains());
    }

    private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
        List<SecurityWebFilterChain> result = this.securityWebFilterChains;
        if (ObjectUtils.isEmpty(result)) {
            return Arrays.asList(springSecurityFilterChain());
        }
        return result;
    }


public class WebFilterChainProxy implements WebFilter {
    private final List<SecurityWebFilterChain> filters;

    public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
        this.filters = filters; // WebFluxSecurityConfiguration.securityWebFilterChains
    }
    ...
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Flux.fromIterable(this.filters)
                .filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange)).next()
                .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
                .flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
                .map((filters) -> new FilteringWebHandler(chain::filter, filters)).map(DefaultWebFilterChain::new)
                .flatMap((securedChain) -> securedChain.filter(exchange));  // securedChain is `DefaultWebFilterChain`
    }
    ...
}
@AutoConfiguration(after = { WebFluxAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnMissingBean(HttpHandler.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class HttpHandlerAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    public static class AnnotationConfig {

        ...

        @Bean
        public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {
            HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();
            ...
            return httpHandler;
        }

    }

}

public final class WebHttpHandlerBuilder{
    public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
        ...
        // set this.filters
        List<WebFilter> webFilters = context
                .getBeanProvider(WebFilter.class)
                .orderedStream()
                .collect(Collectors.toList());
        builder.filters(filters -> filters.addAll(webFilters));
    }
    public HttpHandler build() {
        WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
        decorated = new ExceptionHandlingWebHandler(decorated,  this.exceptionHandlers);

        HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
        ...
        // return adapted
        return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
    }
}

public class FilteringWebHandler extends WebHandlerDecorator 
    public FilteringWebHandler(WebHandler handler, List<WebFilter> filters) {
        super(handler);
        this.chain = new DefaultWebFilterChain(handler, filters);
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.chain.filter(exchange);
    }

public class DefaultWebFilterChain implements WebFilterChain
    public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
        Assert.notNull(handler, "WebHandler is required");
        this.allFilters = Collections.unmodifiableList(filters);
        this.handler = handler;
        DefaultWebFilterChain chain = initChain(filters, handler);
        this.currentFilter = chain.currentFilter;
        this.chain = chain.chain;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange) {
        return Mono.defer(() ->
                this.currentFilter != null && this.chain != null ?
                        invokeFilter(this.currentFilter, this.chain, exchange) :
                        this.handler.handle(exchange));
    }
    private Mono<Void> invokeFilter(WebFilter current, DefaultWebFilterChain chain, ServerWebExchange exchange) {
        String currentName = current.getClass().getName();
        return current.filter(exchange, chain).checkpoint(currentName + " [DefaultWebFilterChain]");
    }

requst 를 처리시점에 호출되는 filter()

  1. HttpWebHandlerAdapter에서 getDelegate().handle(exchange)
  2. ExceptionHandlingWebHandler.handle() 에서 super.handle() 을 호출
  3. FilteringWebHandler.handle()을 호출하게 된다.
  4. FilteringWebHandler.handle()에서 DefaultWebFilterChain.filter(exchange) 를 호출
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler 
    @Override
    public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
        ...
        ServerWebExchange exchange = createExchange(request, response);
        ...
        return getDelegate().handle(exchange)
                .doOnSuccess(aVoid -> logResponse(exchange))
                .onErrorResume(ex -> handleUnresolvedError(exchange, ex))
                .then(Mono.defer(response::setComplete));
    }
public class ExceptionHandlingWebHandler extends WebHandlerDecorator 
    ...
    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        Mono<Void> completion;
        try {
            // WebHandlerDecorator.handle --> FilteringWebHandler.handle
            completion = super.handle(exchange);
        }
        catch (Throwable ex) {
            ...
        }

        for (WebExceptionHandler handler : this.exceptionHandlers) {
            completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
        }
        return completion;
    }

public class WebHandlerDecorator implements WebHandler

    private final WebHandler delegate;

    public WebHandlerDecorator(WebHandler delegate) {
        ...
        this.delegate = delegate;
    }
    ...

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.delegate.handle(exchange);
    }

/login 호출

curl http://localhost:8888/login
  • DefaultWebFilterChain.filter
  • –> AuthenticationWebFilter.filter
  • --> AuthenticationWebFilter.authenticate()
  • --> authenticationManager.authenticate(token)
  • --> AbstractUserDetailsReactiveAuthenticationManager.authenticate

[컴] ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

openAI 이슈 / 챗gpt /

ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

2023년 3월 20일, chatgpt에서 특정사용자가 채팅기록 사이드바에서 다른 사용자의 대화에 대한 간략한 설명을 볼 수 있었다.(ref. 3)

redis-py library 의 이슈로 다른 사용자의 개인 정보와 채팅 제목이 노출되었다고 한다.

redis-py library 의 이슈

github issue 에 가면, 아래와 같은 재현 code를 확인할 수 있다.

import asyncio
from redis.asyncio import Redis
import sys


async def main():
    myhost, mypassword = sys.argv[1:]
    async with Redis(host=myhost, password=mypassword, ssl=True, single_connection_client=True) as r:

        await r.set('foo', 'foo')
        await r.set('bar', 'bar')

        t = asyncio.create_task(r.get('foo'))
        await asyncio.sleep(0.001)
        t.cancel()
        try:
            await t
            print('try again, we did not cancel the task in time')
        except asyncio.CancelledError:
            print('managed to cancel the task, connection is left open with unread response')

        print('bar:', await r.get('bar'))
        print('ping:', await r.ping())
        print('foo:', await r.get('foo'))


if __name__ == '__main__':
    asyncio.run(main())
$ python redis_cancel.py hostname password
managed to cancel the task, connection is left open with unread response
bar: b'foo'
ping: False
foo: b'PONG'

대략 그림으로 설명하면 이런 모습이다.

redis-py library 의 comuunicatino queue

위 그림에서 ’파란색’부분이 t에서 사용했던 부분인데, 이것이 connection 이 cancel 됐을때도 계속 남아있어서, 그 다음 command 인 r.get('bar') 를 send 할 때, r.get('foo') 가 대신 보내지는 것인 듯 하다. 그래서 결과가 하나씩 밀려서 나온다.

Reference

  1. OpenAI Reveals Redis Bug Behind ChatGPT User Data Exposure Incident, 2023-03-25
  2. Off by 1 - Canceling async Redis command leaves connection open, in unsafe state for future commands · Issue #2624 · redis/redis-py · GitHub
  3. ChatGPT Users Report Being Able to See Random People’s Chat Histories

[컴] node v20.x 에서 TLS v1.2 사용

node v20.x 에서 TLS v1.2 사용

댓글fetch, axios 에선 TLS v1.2 를 사용할 수 없다고 한다.

axios 는 모르겠지만, fetch 에선 TLS v1.2 로 통신하지 못했다. 그래서 https 를 사용해서 해결했다.

node, SSL_OP_LEGACY_SERVER_CONNECT

https.Agent

axios 나 fetch 나 이방법으로 해결이 가능하다.

const httpsAgent = new https.Agent({
  // for self signed you could also add
  // rejectUnauthorized: false,
  
  // allow legacy server
  secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
});
const response = await fetch(url, {
  agent: httpsAgent,
});

const html = await response.text();
console.log(html)

실패: secureOptions 없이 node-fetch 에서 https.Agent 를 사용

이 방법은 적어도 windows 에서는 동작하지 않았다.

import https from "https";
const agent = new https.Agent({
  rejectUnauthorized: false
});
fetch(myUrl, { agent });

아래와 같은 error 가 떴다.

reason: write EPROTO 382D0000:error:0A000152:SSL routines:final_renegotiate:unsafe legacy renegotiation disabled:c:\ws\deps\openssl\openssl\ssl\statem\extensions.c:922:

    at ClientRequest.<anonymous> (D:\a\prog\typescript\nextjs\budgetbuddy\budgetbuddybrain\node_modules\node-fetch\lib\index.js:1501:11)
    at ClientRequest.emit (node:events:518:28)
    at TLSSocket.socketErrorListener (node:_http_client:500:9)
    at TLSSocket.emit (node:events:518:28)
    at emitErrorNT (node:internal/streams/destroy:169:8)
    at emitErrorCloseNT (node:internal/streams/destroy:128:3)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  type: 'system',
  errno: 'EPROTO',
  code: 'EPROTO'
}
...

실패: https.request

처음에는 req.on('data', (chunk) => {}) 부분을 실행하지 않았어서 잘 동작하는 줄 알았는데, 아니었다. 이 방법도 에러가 나온다.

const options = {
  hostname: 'www.howsmyssl.com',
  port: 443,
  path: '/a/check',
  method: 'GET',
  secureProtocol: "TLSv1_2_method"
}
const req = await https.request(options)
let responseBody = '';
req.on('data', (chunk) => {
  console.log(chunk)
  responseBody += chunk;
});
req.on('end', () => {
  console.log('end')
  console.log(`BODY: ${responseBody}`);
});
req.on('error', function(e) {
  console.log('problem with request: ' + e.message);
});

Reference

  1. node.js - Configure https agent to allow only TLS1.2 for outgoing requests - Stack Overflow