@@ -601,6 +601,195 @@ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_moves_string_args)
601601 std::string_view(std::string(64 , ' v' )));
602602LT_END_AUTO_TEST (with_header_moves_string_args)
603603
604+ // -----------------------------------------------------------------------
605+ // TASK-012 review-pass: security validation on fluent setters.
606+ //
607+ // with_header, with_footer, with_cookie must reject keys/values that
608+ // contain CR (\r), LF (\n), or NUL (\0) — these characters allow
609+ // HTTP response-header injection (CWE-113). with_status must reject
610+ // codes outside [100, 599] per RFC 9110 §15.
611+ // -----------------------------------------------------------------------
612+
613+ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_rejects_crlf_in_value)
614+ http_response resp = http_response::string(" body" );
615+ bool threw = false ;
616+ try {
617+ resp.with_header (" X-Foo" , " bar\r\n Set-Cookie: evil=1" );
618+ } catch (const std::invalid_argument&) {
619+ threw = true ;
620+ }
621+ LT_CHECK_EQ (threw, true );
622+ LT_END_AUTO_TEST (with_header_rejects_crlf_in_value)
623+
624+ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_rejects_lf_in_value)
625+ http_response resp = http_response::string(" body" );
626+ bool threw = false ;
627+ try {
628+ resp.with_header (" X-Foo" , " bar\n injected" );
629+ } catch (const std::invalid_argument&) {
630+ threw = true ;
631+ }
632+ LT_CHECK_EQ (threw, true );
633+ LT_END_AUTO_TEST (with_header_rejects_lf_in_value)
634+
635+ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_rejects_nul_in_value)
636+ http_response resp = http_response::string(" body" );
637+ bool threw = false ;
638+ try {
639+ resp.with_header (" X-Foo" , std::string (" bar\0 baz" , 7 ));
640+ } catch (const std::invalid_argument&) {
641+ threw = true ;
642+ }
643+ LT_CHECK_EQ (threw, true );
644+ LT_END_AUTO_TEST (with_header_rejects_nul_in_value)
645+
646+ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_rejects_crlf_in_key)
647+ http_response resp = http_response::string(" body" );
648+ bool threw = false ;
649+ try {
650+ resp.with_header (" X-Foo\r\n Evil" , " value" );
651+ } catch (const std::invalid_argument&) {
652+ threw = true ;
653+ }
654+ LT_CHECK_EQ (threw, true );
655+ LT_END_AUTO_TEST (with_header_rejects_crlf_in_key)
656+
657+ LT_BEGIN_AUTO_TEST(http_response_suite, with_footer_rejects_crlf_in_value)
658+ http_response resp = http_response::string(" body" );
659+ bool threw = false ;
660+ try {
661+ resp.with_footer (" X-Footer" , " val\r\n injected" );
662+ } catch (const std::invalid_argument&) {
663+ threw = true ;
664+ }
665+ LT_CHECK_EQ (threw, true );
666+ LT_END_AUTO_TEST (with_footer_rejects_crlf_in_value)
667+
668+ LT_BEGIN_AUTO_TEST(http_response_suite, with_footer_rejects_lf_in_key)
669+ http_response resp = http_response::string(" body" );
670+ bool threw = false ;
671+ try {
672+ resp.with_footer (" X-Footer\n Evil" , " value" );
673+ } catch (const std::invalid_argument&) {
674+ threw = true ;
675+ }
676+ LT_CHECK_EQ (threw, true );
677+ LT_END_AUTO_TEST (with_footer_rejects_lf_in_key)
678+
679+ LT_BEGIN_AUTO_TEST(http_response_suite, with_cookie_rejects_crlf_in_value)
680+ http_response resp = http_response::string(" body" );
681+ bool threw = false ;
682+ try {
683+ resp.with_cookie (" sid" , " abc\r\n Set-Cookie: evil=1" );
684+ } catch (const std::invalid_argument&) {
685+ threw = true ;
686+ }
687+ LT_CHECK_EQ (threw, true );
688+ LT_END_AUTO_TEST (with_cookie_rejects_crlf_in_value)
689+
690+ LT_BEGIN_AUTO_TEST(http_response_suite, with_cookie_rejects_lf_in_name)
691+ http_response resp = http_response::string(" body" );
692+ bool threw = false ;
693+ try {
694+ resp.with_cookie (" sid\n evil" , " value" );
695+ } catch (const std::invalid_argument&) {
696+ threw = true ;
697+ }
698+ LT_CHECK_EQ (threw, true );
699+ LT_END_AUTO_TEST (with_cookie_rejects_lf_in_name)
700+
701+ LT_BEGIN_AUTO_TEST(http_response_suite, with_cookie_rejects_nul_in_value)
702+ http_response resp = http_response::string(" body" );
703+ bool threw = false ;
704+ try {
705+ resp.with_cookie (" sid" , std::string (" abc\0 def" , 7 ));
706+ } catch (const std::invalid_argument&) {
707+ threw = true ;
708+ }
709+ LT_CHECK_EQ (threw, true );
710+ LT_END_AUTO_TEST (with_cookie_rejects_nul_in_value)
711+
712+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_rejects_below_100)
713+ http_response resp = http_response::string(" body" );
714+ bool threw = false ;
715+ try {
716+ resp.with_status (99 );
717+ } catch (const std::invalid_argument&) {
718+ threw = true ;
719+ }
720+ LT_CHECK_EQ (threw, true );
721+ LT_END_AUTO_TEST (with_status_rejects_below_100)
722+
723+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_rejects_above_599)
724+ http_response resp = http_response::string(" body" );
725+ bool threw = false ;
726+ try {
727+ resp.with_status (600 );
728+ } catch (const std::invalid_argument&) {
729+ threw = true ;
730+ }
731+ LT_CHECK_EQ (threw, true );
732+ LT_END_AUTO_TEST (with_status_rejects_above_599)
733+
734+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_rejects_negative)
735+ http_response resp = http_response::string(" body" );
736+ bool threw = false ;
737+ try {
738+ resp.with_status (-1 );
739+ } catch (const std::invalid_argument&) {
740+ threw = true ;
741+ }
742+ LT_CHECK_EQ (threw, true );
743+ LT_END_AUTO_TEST (with_status_rejects_negative)
744+
745+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_rejects_zero)
746+ http_response resp = http_response::string(" body" );
747+ bool threw = false ;
748+ try {
749+ resp.with_status (0 );
750+ } catch (const std::invalid_argument&) {
751+ threw = true ;
752+ }
753+ LT_CHECK_EQ (threw, true );
754+ LT_END_AUTO_TEST (with_status_rejects_zero)
755+
756+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_accepts_boundary_100)
757+ http_response resp = http_response::string(" body" );
758+ bool threw = false ;
759+ try {
760+ resp.with_status (100 );
761+ } catch (const std::invalid_argument&) {
762+ threw = true ;
763+ }
764+ LT_CHECK_EQ (threw, false );
765+ LT_CHECK_EQ (resp.get_status(), 100);
766+ LT_END_AUTO_TEST (with_status_accepts_boundary_100)
767+
768+ LT_BEGIN_AUTO_TEST(http_response_suite, with_status_accepts_boundary_599)
769+ http_response resp = http_response::string(" body" );
770+ bool threw = false ;
771+ try {
772+ resp.with_status (599 );
773+ } catch (const std::invalid_argument&) {
774+ threw = true ;
775+ }
776+ LT_CHECK_EQ (threw, false );
777+ LT_CHECK_EQ (resp.get_status(), 599);
778+ LT_END_AUTO_TEST (with_status_accepts_boundary_599)
779+
780+ LT_BEGIN_AUTO_TEST(http_response_suite, with_header_accepts_valid_value)
781+ http_response resp = http_response::string(" body" );
782+ bool threw = false ;
783+ try {
784+ resp.with_header (" X-Foo" , " valid value with spaces and colons: ok" );
785+ } catch (const std::invalid_argument&) {
786+ threw = true ;
787+ }
788+ LT_CHECK_EQ (threw, false );
789+ LT_CHECK_EQ (resp.get_header(" X-Foo" ),
790+ std::string_view(" valid value with spaces and colons: ok" ));
791+ LT_END_AUTO_TEST (with_header_accepts_valid_value)
792+
604793LT_BEGIN_AUTO_TEST_ENV()
605794 AUTORUN_TESTS()
606795LT_END_AUTO_TEST_ENV()
0 commit comments