Changeset 249810 in webkit


Ignore:
Timestamp:
Sep 12, 2019 8:33:31 AM (5 years ago)
Author:
Adrian Perez de Castro
Message:

[GTK][WPE] webkit_settings_set_user_agent() allows content forbidden in HTTP headers
https://bugs.webkit.org/show_bug.cgi?id=201077

Reviewed by Carlos Garcia Campos.

Source/WebCore:

Add a function to validate whether a string contains a valid value
which can be used in a HTTP User-Agent header.

Covered by new WebCore API test HTTPParsers.ValidateUserAgentValues.

  • platform/glib/UserAgentGLib.cpp:

(WebCore::standardUserAgent): Assert that the returned string is a valid User-Agent.
(WebCore::standardUserAgentForURL): Ditto.

  • platform/network/HTTPParsers.cpp: Added a series of helper functions which skip over

characters of a string, which can be used to scan over the different elements of an
User-Agent value; all of them receive the position from the input string where to start
scanning, updating it to the position right after the scanned item (this follow the
convention already in use by other functions in the source file). Each of them has
been annotated with the RFC number and section which contains the definition of the
scanned item, and the corresponding BNF rules to make the code easier to follow.
(WebCore::skipWhile): Added.
(WebCore::isVisibleCharacter): Added.
(WebCore::isOctectInFieldContentCharacter): Added.
(WebCore::isCommentTextCharacter): Added.
(WebCore::isHTTPTokenCharacter): Added.
(WebCore::isValidHTTPToken): Refactored to use the new isHTTPTokenCharacter()
helper function instead of having the test inside the loop.
(WebCore::skipCharacter): Added.
(WebCore::skipQuotedPair): Added.
(WebCore::skipComment): Added.
(WebCore::skipHTTPToken): Added.
(WebCore::skipUserAgentProduct): Added.
(WebCore::isValidUserAgentHeaderValue): Added.

  • platform/network/HTTPParsers.h: Add prototype for isValidUserAgentHeaderValue().

Source/WebKit:

  • UIProcess/API/glib/WebKitSettings.cpp:

(webkit_settings_set_user_agent): Check the passed string using the new
WebCore::isValidUserAgentHeaderValue() function, and return early without
changing the setting if the string is not usable in the User-Agent HTTP
header.

Tools:

  • TestWebKitAPI/CMakeLists.txt: Add missing HTTPParsers.cpp to be built into TestWebCore.
  • TestWebKitAPI/Tests/WebCore/HTTPParsers.cpp:

(TestWebKitAPI::TEST): Add tests for WebCore::isValidUserAgentHeaderValue().

Location:
trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r249808 r249810  
     12019-09-12  Adrian Perez de Castro  <aperez@igalia.com>
     2
     3        [GTK][WPE] webkit_settings_set_user_agent() allows content forbidden in HTTP headers
     4        https://bugs.webkit.org/show_bug.cgi?id=201077
     5
     6        Reviewed by Carlos Garcia Campos.
     7
     8        Add a function to validate whether a string contains a valid value
     9        which can be used in a HTTP User-Agent header.
     10
     11        Covered by new WebCore API test HTTPParsers.ValidateUserAgentValues.
     12
     13        * platform/glib/UserAgentGLib.cpp:
     14        (WebCore::standardUserAgent): Assert that the returned string is a valid User-Agent.
     15        (WebCore::standardUserAgentForURL): Ditto.
     16        * platform/network/HTTPParsers.cpp: Added a series of helper functions which skip over
     17        characters of a string, which can be used to scan over the different elements of an
     18        User-Agent value; all of them receive the position from the input string where to start
     19        scanning, updating it to the position right after the scanned item (this follow the
     20        convention already in use by other functions in the source file). Each of them has
     21        been annotated with the RFC number and section which contains the definition of the
     22        scanned item, and the corresponding BNF rules to make the code easier to follow.
     23        (WebCore::skipWhile): Added.
     24        (WebCore::isVisibleCharacter): Added.
     25        (WebCore::isOctectInFieldContentCharacter): Added.
     26        (WebCore::isCommentTextCharacter): Added.
     27        (WebCore::isHTTPTokenCharacter): Added.
     28        (WebCore::isValidHTTPToken): Refactored to use the new isHTTPTokenCharacter()
     29        helper function instead of having the test inside the loop.
     30        (WebCore::skipCharacter): Added.
     31        (WebCore::skipQuotedPair): Added.
     32        (WebCore::skipComment): Added.
     33        (WebCore::skipHTTPToken): Added.
     34        (WebCore::skipUserAgentProduct): Added.
     35        (WebCore::isValidUserAgentHeaderValue): Added.
     36        * platform/network/HTTPParsers.h: Add prototype for isValidUserAgentHeaderValue().
     37
    1382019-09-12  Mark Lam  <mark.lam@apple.com>
    239
  • trunk/Source/WebCore/platform/glib/UserAgentGLib.cpp

    r247427 r249810  
    126126    // wrong can cause sites to load the wrong JavaScript, CSS, or custom fonts. In some cases
    127127    // sites won't load resources at all.
    128     if (applicationName.isEmpty())
    129         return standardUserAgentStatic();
    130128
    131     String finalApplicationVersion = applicationVersion;
    132     if (finalApplicationVersion.isEmpty())
    133         finalApplicationVersion = versionForUAString();
    134 
    135     return standardUserAgentStatic() + ' ' + applicationName + '/' + finalApplicationVersion;
     129    String userAgent;
     130    if (applicationName.isEmpty()) {
     131        userAgent = standardUserAgentStatic();
     132    } else {
     133        String finalApplicationVersion = applicationVersion;
     134        if (finalApplicationVersion.isEmpty())
     135            finalApplicationVersion = versionForUAString();
     136        userAgent = standardUserAgentStatic() + ' ' + applicationName + '/' + finalApplicationVersion;
     137    }
     138    ASSERT(isValidUserAgentHeaderValue(userAgent));
     139    return userAgent;
    136140}
    137141
     
    140144    auto quirks = UserAgentQuirks::quirksForURL(url);
    141145    // The null string means we don't need a specific UA for the given URL.
    142     return quirks.isEmpty() ? String() : buildUserAgentString(quirks);
     146    if (quirks.isEmpty())
     147        return String();
     148
     149    String userAgent(buildUserAgentString(quirks));
     150    ASSERT(isValidUserAgentHeaderValue(userAgent));
     151    return userAgent;
    143152}
    144153
  • trunk/Source/WebCore/platform/network/HTTPParsers.cpp

    r249792 r249810  
    9898}
    9999
     100// True if characters which satisfy the predicate are present, incrementing
     101// "pos" to the next character which does not satisfy the predicate.
     102// Note: might return pos == str.length().
     103static inline bool skipWhile(const String& str, unsigned& pos, const WTF::Function<bool(const UChar)>& predicate)
     104{
     105    const unsigned start = pos;
     106    const unsigned len = str.length();
     107    while (pos < len && predicate(str[pos]))
     108        ++pos;
     109    return pos != start;
     110}
     111
    100112// See RFC 7230, Section 3.1.2.
    101113bool isValidReasonPhrase(const String& value)
     
    134146        || c == '<' || c == '=' || c == '>' || c == '?' || c == '@' || c == '[' || c == '\\'
    135147        || c == ']' || c == '{' || c == '}');
     148}
     149
     150// See RFC 7230, Section 3.2.6.
     151static inline bool isVisibleCharacter(const UChar c)
     152{
     153    // VCHAR = %x21-7E
     154    return (c >= 0x21 && c <= 0x7E);
     155}
     156
     157// See RFC 7230, Section 3.2.6.
     158static inline bool isOctectInFieldContentCharacter(const UChar c)
     159{
     160    // obs-text = %x80-FF
     161    return (c >= 0x80 && c <= 0xFF);
     162}
     163
     164// See RFC 7230, Section 3.2.6.
     165static bool isCommentTextCharacter(const UChar c)
     166{
     167    // ctext = HTAB / SP
     168    //       / %x21-27 ; '!'-'''
     169    //       / %x2A-5B ; '*'-'['
     170    //       / %x5D-7E ; ']'-'~'
     171    //       / obs-text
     172    return (c == '\t' || c == ' '
     173        || (c >= 0x21 && c <= 0x27)
     174        || (c >= 0x2A && c <= 0x5B)
     175        || (c >= 0x5D && c <= 0x7E)
     176        || isOctectInFieldContentCharacter(c));
    136177}
    137178
     
    175216
    176217// See RFC 7230, Section 3.2.6.
     218static inline bool isHTTPTokenCharacter(const UChar c)
     219{
     220    // Any VCHAR, except delimiters
     221    return c > 0x20 && c < 0x7F && !isDelimiterCharacter(c);
     222}
     223
     224// See RFC 7230, Section 3.2.6.
    177225bool isValidHTTPToken(const String& value)
    178226{
     
    181229    auto valueStringView = StringView(value);
    182230    for (UChar c : valueStringView.codeUnits()) {
    183         if (c <= 0x20 || c >= 0x7F
    184             || c == '(' || c == ')' || c == '<' || c == '>' || c == '@'
    185             || c == ',' || c == ';' || c == ':' || c == '\\' || c == '"'
    186             || c == '/' || c == '[' || c == ']' || c == '?' || c == '='
    187             || c == '{' || c == '}')
    188         return false;
     231        if (!isHTTPTokenCharacter(c))
     232            return false;
    189233    }
    190234    return true;
     235}
     236
     237// True if the character at the given position satisifies a predicate, incrementing "pos" by one.
     238// Note: Might return pos == str.length()
     239static inline bool skipCharacter(const String& value, unsigned& pos, WTF::Function<bool(const UChar)>&& predicate)
     240{
     241    if (pos < value.length() && predicate(value[pos])) {
     242        ++pos;
     243        return true;
     244    }
     245    return false;
     246}
     247
     248// True if the "expected" character is at the given position, incrementing "pos" by one.
     249// Note: Might return pos == str.length()
     250static inline bool skipCharacter(const String& value, unsigned& pos, const UChar expected)
     251{
     252    return skipCharacter(value, pos, [expected](const UChar c) {
     253        return c == expected;
     254    });
     255}
     256
     257// True if a quoted pair is present, incrementing "pos" to the position after the quoted pair.
     258// Note: Might return pos == str.length()
     259// See RFC 7230, Section 3.2.6.
     260static constexpr auto QuotedPairStartCharacter = '\\';
     261static bool skipQuotedPair(const String& value, unsigned& pos)
     262{
     263    // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
     264    if (!skipCharacter(value, pos, QuotedPairStartCharacter))
     265        return false;
     266
     267    return skipCharacter(value, pos, '\t')
     268        || skipCharacter(value, pos, ' ')
     269        || skipCharacter(value, pos, isVisibleCharacter)
     270        || skipCharacter(value, pos, isOctectInFieldContentCharacter);
     271}
     272
     273// True if a comment is present, incrementing "pos" to the position after the comment.
     274// Note: Might return pos == str.length()
     275// See RFC 7230, Section 3.2.6.
     276static constexpr auto CommentStartCharacter = '(';
     277static constexpr auto CommentEndCharacter = ')';
     278static bool skipComment(const String& value, unsigned& pos)
     279{
     280    // comment = "(" *( ctext / quoted-pair / comment ) ")"
     281    // ctext   = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
     282    if (!skipCharacter(value, pos, CommentStartCharacter))
     283        return false;
     284
     285    const unsigned end = value.length();
     286    while (pos < end && value[pos] != CommentEndCharacter) {
     287        switch (value[pos]) {
     288        case CommentStartCharacter:
     289            if (!skipComment(value, pos))
     290                return false;
     291            break;
     292        case QuotedPairStartCharacter:
     293            if (!skipQuotedPair(value, pos))
     294                return false;
     295            break;
     296        default:
     297            if (!skipWhile(value, pos, isCommentTextCharacter))
     298                return false;
     299        }
     300    }
     301    return skipCharacter(value, pos, CommentEndCharacter);
     302}
     303
     304// True if an HTTP header token is present, incrementing "pos" to the position after it.
     305// Note: Might return pos == str.length()
     306// See RFC 7230, Section 3.2.6.
     307static bool skipHTTPToken(const String& value, unsigned& pos)
     308{
     309    return skipWhile(value, pos, isHTTPTokenCharacter);
     310}
     311
     312// True if a product specifier (as in an User-Agent header) is present, incrementing "pos" to the position after it.
     313// Note: Might return pos == str.length()
     314// See RFC 7231, Section 5.5.3.
     315static bool skipUserAgentProduct(const String& value, unsigned& pos)
     316{
     317    // product         = token ["/" product-version]
     318    // product-version = token
     319    if (!skipHTTPToken(value, pos))
     320        return false;
     321    if (skipCharacter(value, pos, '/'))
     322        return skipHTTPToken(value, pos);
     323    return true;
     324}
     325
     326// See RFC 7231, Section 5.5.3
     327bool isValidUserAgentHeaderValue(const String& value)
     328{
     329    // User-Agent = product *( RWS ( product / comment ) )
     330    unsigned pos = 0;
     331    if (!skipUserAgentProduct(value, pos))
     332        return false;
     333
     334    while (pos < value.length()) {
     335        if (!skipWhiteSpace(value, pos))
     336            return false;
     337        if (value[pos] == CommentStartCharacter) {
     338            if (!skipComment(value, pos))
     339                return false;
     340        } else {
     341            if (!skipUserAgentProduct(value, pos))
     342                return false;
     343        }
     344    }
     345
     346    return pos == value.length();
    191347}
    192348
  • trunk/Source/WebCore/platform/network/HTTPParsers.h

    r249792 r249810  
    7373bool isValidAcceptHeaderValue(const String&);
    7474bool isValidLanguageHeaderValue(const String&);
     75WEBCORE_EXPORT bool isValidUserAgentHeaderValue(const String&);
    7576bool isValidHTTPToken(const String&);
    7677Optional<WallTime> parseHTTPDate(const String&);
  • trunk/Source/WebKit/ChangeLog

    r249808 r249810  
     12019-09-12  Adrian Perez de Castro  <aperez@igalia.com>
     2
     3        [GTK][WPE] webkit_settings_set_user_agent() allows content forbidden in HTTP headers
     4        https://bugs.webkit.org/show_bug.cgi?id=201077
     5
     6        Reviewed by Carlos Garcia Campos.
     7
     8        * UIProcess/API/glib/WebKitSettings.cpp:
     9        (webkit_settings_set_user_agent): Check the passed string using the new
     10        WebCore::isValidUserAgentHeaderValue() function, and return early without
     11        changing the setting if the string is not usable in the User-Agent HTTP
     12        header.
     13
    1142019-09-12  Mark Lam  <mark.lam@apple.com>
    215
  • trunk/Source/WebKit/UIProcess/API/glib/WebKitSettings.cpp

    r246353 r249810  
    3636#include "WebPageProxy.h"
    3737#include "WebPreferences.h"
     38#include <WebCore/HTTPParsers.h>
    3839#include <WebCore/PlatformScreen.h>
    3940#include <WebCore/TextEncodingRegistry.h>
     
    30443045
    30453046    WebKitSettingsPrivate* priv = settings->priv;
    3046     CString newUserAgent = (!userAgent || !strlen(userAgent)) ? WebCore::standardUserAgent("").utf8() : userAgent;
     3047
     3048    String userAgentString;
     3049    if (userAgent && *userAgent) {
     3050        userAgentString = String::fromUTF8(userAgent);
     3051        g_return_if_fail(WebCore::isValidUserAgentHeaderValue(userAgentString));
     3052    } else
     3053        userAgentString = WebCore::standardUserAgent("");
     3054
     3055    CString newUserAgent = userAgentString.utf8();
    30473056    if (newUserAgent == priv->userAgent)
    30483057        return;
  • trunk/Tools/ChangeLog

    r249808 r249810  
     12019-09-12  Adrian Perez de Castro  <aperez@igalia.com>
     2
     3        [GTK][WPE] webkit_settings_set_user_agent() allows content forbidden in HTTP headers
     4        https://bugs.webkit.org/show_bug.cgi?id=201077
     5
     6        Reviewed by Carlos Garcia Campos.
     7
     8        * TestWebKitAPI/CMakeLists.txt: Add missing HTTPParsers.cpp to be built into TestWebCore.
     9        * TestWebKitAPI/Tests/WebCore/HTTPParsers.cpp:
     10        (TestWebKitAPI::TEST): Add tests for WebCore::isValidUserAgentHeaderValue().
     11
    1122019-09-12  Mark Lam  <mark.lam@apple.com>
    213
  • trunk/Tools/TestWebKitAPI/CMakeLists.txt

    r249589 r249810  
    131131        Tests/WebCore/GridPosition.cpp
    132132        Tests/WebCore/HTMLParserIdioms.cpp
     133        Tests/WebCore/HTTPParsers.cpp
    133134        Tests/WebCore/IntPoint.cpp
    134135        Tests/WebCore/IntRect.cpp
  • trunk/Tools/TestWebKitAPI/Tests/WebCore/HTTPParsers.cpp

    r233739 r249810  
    5757}
    5858
     59TEST(HTTPParsers, ValidateUserAgentValues)
     60{
     61    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari"));
     62    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari WebKit"));
     63    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari/10.0"));
     64    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari WebKit/163"));
     65    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari/10.0 WebKit"));
     66    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari/10.0 WebKit/163"));
     67    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari/10.0 WebKit/163 (Mozilla; like Gecko)"));
     68    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari (comment (nested comment))"));
     69    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari () (<- Empty comment)"));
     70    EXPECT_TRUE(isValidUserAgentHeaderValue("Safari (left paren \\( as quoted pair)"));
     71    EXPECT_TRUE(isValidUserAgentHeaderValue("!#$%&'*+-.^_`|~ (non-alphanumeric token characters)"));
     72    EXPECT_TRUE(isValidUserAgentHeaderValue("0123456789 (numeric token characters)"));
     73    EXPECT_TRUE(isValidUserAgentHeaderValue("a (single character token)"));
     74
     75    EXPECT_FALSE(isValidUserAgentHeaderValue(" "));
     76    EXPECT_FALSE(isValidUserAgentHeaderValue(" Safari (leading whitespace)"));
     77    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari (trailing whitespace) "));
     78    EXPECT_FALSE(isValidUserAgentHeaderValue("\nSafari (leading newline)"));
     79    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari (trailing newline)\n"));
     80    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari/ (no version token after slash)"));
     81    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari (unterminated comment"));
     82    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari unopened commanent)"));
     83    EXPECT_FALSE(isValidUserAgentHeaderValue("\x1B (contains control character)"));
     84    EXPECT_FALSE(isValidUserAgentHeaderValue("Safari/\n10.0 (embeded newline)"));
     85    EXPECT_FALSE(isValidUserAgentHeaderValue("WPE\\ WebKit (quoted pair in token)"));
     86    EXPECT_FALSE(isValidUserAgentHeaderValue("/123 (missing product token)"));
     87}
     88
    5989} // namespace TestWebKitAPI
Note: See TracChangeset for help on using the changeset viewer.