|
1 | 1 | ////////////////////////////////////////////////////////////////////////// |
2 | 2 | // |
3 | 3 | // TinyWeb |
4 | | -// Copyright (C) 2021-2023 Maxim Masiutin |
| 4 | +// Copyright (C) 2021-2025 Maxim Masiutin |
5 | 5 | // Copyright (C) 2000-2017 RITLABS S.R.L. |
6 | 6 | // Copyright (C) 1997-2000 RIT Research Labs |
7 | 7 | // |
@@ -796,6 +796,71 @@ procedure TPipeReadStdThread.Execute; |
796 | 796 | until False; |
797 | 797 | end; |
798 | 798 |
|
| 799 | +// CGI Query Parameter Security Functions |
| 800 | +// Implements defense-in-depth for ISINDEX-style queries per RFC 3875 Section 4.4 |
| 801 | +// Reference: https://datatracker.ietf.org/doc/html/rfc3875#section-4.4 |
| 802 | +// |
| 803 | +// Two layers of protection: |
| 804 | +// 1. Whitelist validation (optional, enabled by STRICT_CGI_PARAMS define) |
| 805 | +// 2. Apache-style shell metacharacter escaping (always active) |
| 806 | + |
| 807 | +{$IFDEF STRICT_CGI_PARAMS} |
| 808 | +// Whitelist validation: Only allow safe characters in CGI query parameters |
| 809 | +// Returns True if the parameter contains only safe characters |
| 810 | +// This is the first layer - rejects requests with unsafe characters |
| 811 | +function IsQueryParamSafe(const s: AnsiString): Boolean; |
| 812 | +var |
| 813 | + i: Integer; |
| 814 | + c: AnsiChar; |
| 815 | +begin |
| 816 | + Result := True; |
| 817 | + for i := 1 to Length(s) do |
| 818 | + begin |
| 819 | + c := s[i]; |
| 820 | + // Allow: A-Z, a-z, 0-9, hyphen, underscore, dot, forward slash, backslash, colon |
| 821 | + if not ((c >= 'A') and (c <= 'Z')) and |
| 822 | + not ((c >= 'a') and (c <= 'z')) and |
| 823 | + not ((c >= '0') and (c <= '9')) and |
| 824 | + (c <> '-') and (c <> '_') and (c <> '.') and |
| 825 | + (c <> '/') and (c <> '\') and (c <> ':') then |
| 826 | + begin |
| 827 | + Result := False; |
| 828 | + Exit; |
| 829 | + end; |
| 830 | + end; |
| 831 | +end; |
| 832 | +{$ENDIF} |
| 833 | + |
| 834 | +// Apache-style shell metacharacter escaping for Windows |
| 835 | +// Escapes dangerous characters with caret (^) and wraps in quotes |
| 836 | +// Based on Apache httpd ap_escape_shell_cmd() and PHP escapeshellcmd() |
| 837 | +// This is the second layer - always active as defense-in-depth |
| 838 | +// Reference: https://datatracker.ietf.org/doc/html/rfc3875#section-7.2 |
| 839 | +function EscapeShellParam(const s: AnsiString): AnsiString; |
| 840 | +const |
| 841 | + // Windows shell metacharacters that need escaping |
| 842 | + DangerousChars = '&|<>^()%!"''`;$[]{}*?~'; |
| 843 | +var |
| 844 | + i: Integer; |
| 845 | + c: AnsiChar; |
| 846 | +begin |
| 847 | + Result := ''; |
| 848 | + for i := 1 to Length(s) do |
| 849 | + begin |
| 850 | + c := s[i]; |
| 851 | + // Block newlines entirely - cannot be safely escaped on Windows |
| 852 | + if (c = #10) or (c = #13) then |
| 853 | + Continue; |
| 854 | + // Escape dangerous characters with caret (Windows cmd.exe escape char) |
| 855 | + if Pos(c, DangerousChars) > 0 then |
| 856 | + Result := Result + '^'; |
| 857 | + Result := Result + c; |
| 858 | + end; |
| 859 | + // Wrap in quotes for additional safety |
| 860 | + if Result <> '' then |
| 861 | + Result := '"' + Result + '"'; |
| 862 | +end; |
| 863 | + |
799 | 864 | function ExecuteScript(const AExecutable, APath, AScript, AQueryParam, AEnvStr, |
800 | 865 | AStdInStr: AnsiString; Buffer: THTTPServerThreadBuffer; SelfThr: TThread; |
801 | 866 | var ErrorMsg: AnsiString): TEntityHeader; |
@@ -852,8 +917,9 @@ function ExecuteScript(const AExecutable, APath, AScript, AQueryParam, AEnvStr, |
852 | 917 | s := AExecutable |
853 | 918 | else |
854 | 919 | s := AExecutable + ' ' + AScript; |
| 920 | + // Apply Apache-style escaping to query parameter (always active) |
855 | 921 | if AQueryParam <> '' then |
856 | | - s := s + ' ' + AQueryParam; |
| 922 | + s := s + ' ' + EscapeShellParam(AQueryParam); |
857 | 923 | s := DelSpaces(s); |
858 | 924 | if (s = '') or (AEnvStr = '') or (APath = '') then |
859 | 925 | begin |
@@ -1921,12 +1987,21 @@ procedure THTTPServerThread.Execute; |
1921 | 1987 | CEqual := '='; |
1922 | 1988 | if Pos(CEqual, URIQuery) = 0 then |
1923 | 1989 | begin |
| 1990 | + // ISINDEX-style query (no '=' sign) per RFC 3875 Section 4.4 |
1924 | 1991 | URIQueryParam := URIQuery; |
1925 | 1992 | if not UnpackPchars(URIQueryParam) then |
1926 | 1993 | Break; |
1927 | 1994 | CZero := #0; |
1928 | 1995 | if Pos(CZero, URIQueryParam) > 0 then |
1929 | 1996 | Break; |
| 1997 | +{$IFDEF STRICT_CGI_PARAMS} |
| 1998 | + // Whitelist validation: reject parameters with unsafe characters |
| 1999 | + if not IsQueryParamSafe(URIQueryParam) then |
| 2000 | + begin |
| 2001 | + StatusCode := 400; |
| 2002 | + Break; |
| 2003 | + end; |
| 2004 | +{$ENDIF} |
1930 | 2005 | end; |
1931 | 2006 | end; |
1932 | 2007 | CSemicolon := ';'; |
|
0 commit comments