Wordpress จับคู่ URL กับตัวหนอนต่อท้าย


11

ฉันได้รับรายงานช่องโหว่ (1) ที่ดูเหมือนว่ามีความหมายว่าอาจมีปัญหาด้านความปลอดภัยในวิธีที่ Wordpress จัดการกับ URL ด้วยเครื่องหมายตัวหนอนต่อไปนี้ ดูเหมือนว่าสแกนเนอร์คิดว่าเว็บไซต์อาจให้บริการบางรายการไดเรกทอรีและเช่น

ฉันประหลาดใจที่เว็บไซต์ของฉันยังคงให้บริการเนื้อหาใน URL ที่แตกต่างกันดังนั้นฉันจึงทำการทดสอบโดยการติดตั้งอินสแตนซ์ WP ว่างเปล่าทั้งหมดเปลี่ยนเป็น Permalinks "ชื่อโพสต์" และยืนยันว่า yep URL ใด ๆ ที่มีเครื่องหมายตัวหนอน URL ที่ไม่มีเครื่องหมายตัวหนอน

แท้จริงแล้ว URL เช่นนี้:

https://mywordpresssite.com/my-permalink

สามารถเข้าถึงได้ด้วย URL ต่อไปนี้:

https://mywordpresssite.com/my-permalink~
https://mywordpresssite.com/my-permalink~/
https://mywordpresssite.com/my-permalink~~~~~~

ฉันแหย่เล็กน้อยเพื่อดูว่า WP แยกวิเคราะห์ Permalinks ไว้ที่ใดและฉันติดตามลงไปclass-wp.phpในparse_requestวิธีการ แต่ไม่สามารถไปไกลกว่านั้นได้อีก

คำถามของฉันคือถ้าสิ่งนี้เป็นพฤติกรรมที่ตั้งใจไว้สำหรับ WP และถ้าเป็นเช่นนั้นมีวิธีใดบ้างที่ฉันจะปิดการทำงานนี้เพื่อไม่ให้จับคู่ตัวหนอนได้ เหตุใด WP จึงตีความ URL ด้วย tildes เป็น URL ที่ไม่มี URL

(1) อ๋อตอนนี้เราได้เห็นการแฮ็กข้อมูลและการรั่วไหลของข้อมูลที่สำคัญในสหราชอาณาจักรเป็นเวลานั้นอีกครั้งที่พวก "ความปลอดภัย" แกล้งทำเป็นว่าพวกเขากำลังทำบิตของพวกเขาโดยส่งรายงานการสแกน 200 หน้าให้นักพัฒนา เต็มไปด้วยปัญหาเชิงบวกและปัญหาทั่วไปพวกเขาไม่รู้อะไรเลยเกี่ยวกับความคาดหวังถ้าเราอ่านและดำเนินการตามรายงานที่กล่าวมาจะไม่มีอะไรเลวร้ายเกิดขึ้น

คำตอบ:


13

ไปง่าย ๆ

ถ้าฉันเข้าใจ OP เป็นอย่างดีปัญหาของคุณก็คือ URL ที่มีเครื่องหมายตัวหนอนถูกจับคู่ไว้เลย

คำตอบอื่น ๆ ทั้งหมดมุ่งเน้นไปที่ความจริงที่ว่าการทำให้บริสุทธิ์สำหรับการสืบค้นแยกอักขระบางตัวออกก่อนที่จะดำเนินการค้นหา แต่ควรจะป้องกันไม่ให้กฎการเขียนซ้ำไม่ตรงกันในบางสถานการณ์

และเป็นไปได้ที่จะทำได้ไม่ง่ายมาก แต่ทำได้

ทำไมมันตรงกันในตอนแรก?

เหตุผลที่ URL สองรายการที่เหมือนกันexample.com/postnameและexample.com/postname~ตรงกับกฎการเขียนซ้ำเดียวกันนั้นเป็นเพราะกฎการเขียนซ้ำ WP สำหรับโพสต์ใช้แท็กการเขียน%postname%ที่ถูกแทนที่ด้วย regex ([^/]+)เมื่อสร้างกฎการเขียนซ้ำ

ปัญหาคือ regex ([^/]+)จับคู่กับชื่อโพสต์postname~และเนื่องจากการฆ่าเชื้อชื่อที่สอบถามจะpostnameสิ้นสุดลงในผลลัพธ์ที่ถูกต้อง

หมายความว่าหากเราสามารถเปลี่ยน regex จาก([^/]+)เป็น([^~/]+)tilde จะไม่ตรงกันอีกต่อไปดังนั้นเราจึงป้องกัน URL ที่มี tilde ในชื่อโพสต์จับคู่ไม่ได้

เนื่องจากไม่มีกฎใดจะจับคู่กัน URL จะเป็น 404 ซึ่งควรเป็นพฤติกรรมที่คาดหวัง

ป้องกันการจับคู่

add_rewrite_tag%postname%เป็นฟังก์ชันที่แม้จะมีชื่อของมันสามารถนำมาใช้ในการปรับปรุงป้ายเขียนที่มีอยู่เช่น

ดังนั้นถ้าเราใช้รหัส:

add_action('init', function() {
  add_rewrite_tag( '%postname%', '([^~/]+)', 'name=' );
});

เราจะไปถึงเป้าหมายของเราและexample.com/postname~จะไม่example.com/postnameตรงกับกฎสำหรับ

ดังนั้นใช่3 เส้นข้างต้นเป็นรหัสเดียวที่คุณจะต้อง

อย่างไรก็ตามก่อนที่จะใช้งานได้คุณจะต้องล้างข้อมูลกฎการเขียนซ้ำโดยไปที่หน้าการตั้งค่าลิงก์ในแบ็กเอนด์

โปรดทราบว่า regex ([^~/]+)ป้องกันไม่ให้ตัวหนอนอยู่ที่ใดก็ตามในชื่อโพสต์ไม่เพียง แต่เป็นตัวอักษรต่อท้าย แต่เนื่องจากชื่อโพสต์ไม่สามารถมีตัวหนอนเพราะการฆ่าเชื้อที่ควรจะเป็นปัญหา


1
+1 เหมือนความเรียบง่าย ;-) มันก็ดูเหมือนว่าเราสามารถปรับมันสำหรับตัวอักษรอื่น ๆ ได้เช่นกัน
Birgire

1
@ Birgire เราไม่ได้ทั้งหมดหรือไม่ ;)
gmazzap

@ Birgire ใช่เราสามารถป้องกันไม่ให้ตัวละครใด ๆ ถูกถอดออกได้sanitize_titleแต่เนื่องจากสามารถกรองได้จึงไม่สามารถเขียนโซลูชันที่ถูกต้องได้เสมอ ดังนั้นฉันจึงเจาะจง
gmazzap

1
คำตอบนี้มีทางออกที่สะอาดที่สุดและอธิบายปัญหาที่เรากำลังเผชิญอยู่อย่างชัดเจน ขอบคุณมากสำหรับคุณ!
dKen

7

เป็นพฤติกรรมที่ตั้งใจไว้สำหรับ WP

ใช่ดังที่อธิบายไว้แล้วWP_Query::get_posts()ใช้sanitize_title_for_query()( ซึ่งใช้sanitize_title() ) ในการฆ่าเชื้อชื่อโพสต์ของโพสต์เอกพจน์

ในระยะสั้นหลังชื่อโพสต์ผ่านsanitize_title_for_query(), my-permalink === my-permalink~~~เป็นลบต่อท้ายsanitize_title_for_query() ~~~คุณสามารถทดสอบสิ่งนี้ได้โดยทำสิ่งต่อไปนี้:

echo  sanitize_title_for_query( 'my-permalink~~~' )

มีวิธีใดบ้างที่ฉันจะปิดการทำงานนี้เพื่อไม่ให้จับคู่ตัวหนอนได้

นี่ไม่ใช่สิ่งที่คุณสามารถปิดได้ มีตัวกรองที่sanitize_title()เรียกว่าsanitize_titleซึ่งคุณสามารถใช้เพื่อปรับเปลี่ยนพฤติกรรมของsanitize_title()แต่นั่นก็ไม่ใช่ความคิดที่ดีเสมอไป การฉีด SQL นั้นร้ายแรงมากดังนั้นการปล่อยให้บางสิ่งผ่านรอยแตกเนื่องจากการสุขาภิบาลที่ไม่ดีสามารถมีอิทธิพลต่อความสมบูรณ์ของเว็บไซต์ของคุณได้ "เหนือสุขอนามัย" บางครั้งอาจเจ็บปวดในก้น

ฉันไม่แน่ใจว่าคุณเป็นใคร แต่ฉันสงสัยว่าคุณอาจต้องการ 404 โพสต์เดียวกับตัวหนอนตัวต่อท้ายเหล่านี้ในคำพูดของคุณ "ปิดมัน" วิธีเดียวที่ฉันสามารถนึกถึงในขั้นตอนนี้คือการหยุดการสืบค้นหลักเมื่อเรามีเครื่องหมายตัวหนอนต่อท้าย สำหรับสิ่งนี้เราสามารถกรองposts_whereส่วนของแบบสอบถามหลัก

ตัวกรอง

หมายเหตุ: ฉันพิจารณาเฉพาะโพสต์เอกพจน์ปกติและไม่ใช่หน้าคงที่หรือสิ่งที่แนบมาคุณสามารถขยายตัวกรองเพื่อรวมสิ่งนี้

add_filter( 'posts_where', function ( $where, \WP_Query $q )
{
    // Only apply the filter on the main query
    if ( !$q->is_main_query() )
        return $where;

    // Only apply the filter on singular posts
    if ( !$q->is_singular() )
        return $where;

    // We are on a singular page, lets get the singular post name
    $name = sanitize_title_for_query( $q->query_vars['name'] );

    // Suppose $name is empty, like on ugly permalinks, lets bail and let WorPress handle it from here
    if ( !$name )
        return $where;

    // Get the single post URL
    $single_post_url = home_url( add_query_arg( [] ) );
    $parsed_url      = parse_url( $single_post_url );

    // Explode the url and return the page name from the path
    $exploded_pieces = explode( '/',  $parsed_url['path'] );
    $exploded_pieces = array_reverse( $exploded_pieces );

    // Loop through the pieces and return the part holding the pagename
    $raw_name = '';
    foreach ( $exploded_pieces as $piece ) {
        if ( false !== strpos( $piece, $name ) ) {
            $raw_name = $piece;

            break;
        }
    }

    // If $raw_name is empty, we have a serious stuff-up, lets bail and let WordPress handle this mess
    if ( !$raw_name )
        return $where;

    /**
     * All we need to do now is to match $name against $raw_name. If these two don't match,
     * we most probably have some extra crap in the post name/URL. We need to 404, even if the
     * the sanitized version of $raw_name would match $name. 
     */
    if ( $raw_name === $name )
        return $where;

    // $raw_name !== $name, lets halt the main query and 404
    $where .= " AND 0=1 ";

    // Remove the redirect_canonical action so we do not get redirected to the correct URL due to the 404
    remove_action( 'template_redirect', 'redirect_canonical' );

    return $where;
}, 10, 2 );

ไม่กี่หมายเหตุ

ตัวกรองด้านบนจะแสดงหน้า 404 เมื่อเรามี URL ที่เหมือนhttps://mywordpresssite.com/my-permalink~~~~~~กัน อย่างไรก็ตามคุณสามารถลบออกremove_action( 'template_redirect', 'redirect_canonical' );จากตัวกรองได้โดยมีการเปลี่ยนเส้นทางแบบสอบถามโดยอัตโนมัติhttps://mywordpresssite.com/my-permalinkและแสดงโพสต์เดียวเนื่องจากredirect_canonical()มีการติดยาเสพติดtemplate_redirectซึ่งจัดการการเปลี่ยนเส้นทางของ WordPress ที่สร้างขึ้น 404


7

ใช่ดูเหมือนแปลกที่เราควรจะมีการแข่งขันแบบเดียวกันสำหรับ:

example.tld/2016/03/29/test/

และเช่น

example.tld/2016/03/29/..!!$$~~test~~!!$$../

สาเหตุที่เป็นไปได้ดูเหมือนเป็นส่วนหนึ่งของWP_Query::get_posts()วิธีการนี้:

if ( '' != $q['name'] ) {
    $q['name'] = sanitize_title_for_query( $q['name'] );

ที่sanitize_title_for_query()ถูกกำหนดเป็น:

function sanitize_title_for_query( $title ) {
        return sanitize_title( $title, '', 'query' );
}

มันเป็นไปได้ที่จะทำให้sanitize_titleตัวกรองนี้มีความเข้มงวดมากขึ้นแต่อาจไม่ควรแทนที่เอาท์พุทที่เป็นค่าเริ่มต้นซึ่งขึ้นอยู่กับ sanitize_title_with_dashesว่าเป็นผู้รับผิดชอบด้านสุขอนามัยที่นี่ คุณควรพิจารณาสร้างตั๋วแทนการเปลี่ยนแปลงหากไม่มีปัจจุบันอยู่แล้วเกี่ยวกับพฤติกรรมนี้

ปรับปรุง

ฉันสงสัยว่าเราสามารถล้างข้อมูลเสียงจากเส้นทางปัจจุบันด้วยsanitize_title_for_query()และเปลี่ยนเส้นทางไปยัง URL ที่ทำความสะอาดได้ไหมถ้าจำเป็น?

นี่คือตัวอย่างที่คุณสามารถเล่นกับไซต์ทดสอบของคุณและปรับให้เข้ากับความต้องการของคุณ:

/**
 * DEMO: Remove noise from url and redirect to the cleaned version if needed 
 */
add_action( 'init', function( )
{
    // Only for the front-end
    if( is_admin() )
        return;

    // Get current url
    $url = home_url( add_query_arg( [] ) );

    // Let's clean the current path with sanitize_title_for_query()
    $parse = parse_url( $url );
    $parts = explode( '/',  $parse['path'] );
    $parts = array_map( 'sanitize_title_for_query', $parts );   
    $path_clean = join( '/', $parts );
    $url_clean = home_url( $path_clean );
    if( ! empty( $parse['query'] ) )
        $url_clean .= '?' . $parse['query'];

    // Only redirect if the current url is noisy
    if( $url === $url_clean )
        return;
    wp_safe_redirect( esc_url_raw( $url_clean ) );
    exit;
} );

อาจเป็นการดีกว่าหากใช้sanitize_title_with_dashes()โดยตรงเพื่อหลีกเลี่ยงตัวกรองและแทนที่:

$parts = array_map( 'sanitize_title_for_query', $parts );

ด้วย:

foreach( $parts as &$part )
{
    $part = sanitize_title_with_dashes( $part, '', 'query' );
}

PS: ฉันคิดว่าฉันได้เรียนรู้เคล็ดลับนี้เพื่อให้ได้เส้นทางที่ว่างเปล่าadd_query_arg( [] )จาก @gmazzap ;-) นี่เป็นข้อสังเกตใน Codex เช่นกัน ขอขอบคุณอีกครั้งที่ @gmazzap สำหรับคำเตือนการใช้esc_url()เมื่อแสดงผลของadd_query_arg( [] )หรือesc_url_raw()เมื่อเช่นเปลี่ยนเส้นทาง ตรวจสอบการอ้างอิง Codex ก่อนหน้านี้เช่นกัน


+1 เพื่ออธิบายให้ชัดเจนตัวละครพิเศษเหล่านั้นจะถูกลบออกดังนั้นแม้ว่า URL แปลก ๆ จะปรากฏในแถบตำแหน่ง แต่ WordPress จะทำงานกับ URL จริงซึ่งเป็นสาเหตุที่คำขอทำงานได้ตั้งแต่แรก ฉันไม่เห็นความเสี่ยงด้านความปลอดภัยของนายกเทศมนตรีในพฤติกรรมดังกล่าว
Nicolai

1
ใช่ฉันคิดว่าเราไม่ควรยุ่งกับแผ่นกรองสุขาภิบาลเพื่อเปลี่ยน @ialocin นี้
birgire

1
แน่นอนเว้นแต่จะมีเหตุผลที่ดีมากมันเป็นความยุ่งยากไม่คุ้มค่า อย่าพูดว่าเป็นไปได้ยากที่นักพัฒนาสติจะไม่ได้เข้าร่วมในด้านเทคนิค แค่สองเซ็นต์ของฉัน
Nicolai

1
@ Birgire เมื่อใช้งานเช่นนั้นadd_query_argจำเป็นต้องได้รับการหลบหนีด้วยesc_urlหรือesc_url_rawเพื่อป้องกันปัญหาด้านความปลอดภัย ...
gmazzap

อ่าใช่ขอบคุณถ้าผมจำได้อย่างถูกต้องนี้คือปัญหาด้านความปลอดภัยที่พบในหลายปลั๊กอินเมื่อเร็ว ๆ นี้ @gmazzap
birgire

3

ให้ฉันอธิบายการประมวลผลคำขอของ WordPress และวิธีการเปลี่ยนพฤติกรรมของ WordPress เพื่อให้บรรลุเป้าหมายของคุณตามนั้น

แยกการร้องขอ

เมื่อ WordPress ได้รับการร้องขอก็จะเริ่มขั้นตอนการแยกคำขอและแปลงเป็นหน้า หลักของกระบวนการนี้เริ่มต้นเมื่อWP::main()มีการเรียกวิธีการสืบค้นหลักของ WordPress ฟังก์ชันนี้แยกวิเคราะห์แบบสอบถามตามที่คุณระบุอย่างถูกต้องเป็นparse_request()(ในincludes/class-wp.php) มี WordPress พยายามที่จะตรงกับ URL กับหนึ่งในผู้เขียนกฎ เมื่อ URL ถูกจับคู่มันจะสร้างสตริงการสืบค้นของส่วน URL และเข้ารหัสส่วนเหล่านี้ (ทุกอย่างระหว่างสองสแลช) โดยใช้urlencode()เพื่อป้องกันอักขระพิเศษเช่น&จากการทำให้สตริงข้อความค้นหายุ่งเหยิง อักขระที่เข้ารหัสเหล่านี้อาจทำให้คุณคิดว่าปัญหาอยู่ที่นั่น แต่จริง ๆ แล้วพวกเขากลายเป็นอักขระ "จริง" ที่สอดคล้องกันของพวกเขาเมื่อแยกวิเคราะห์สตริงแบบสอบถาม

เรียกใช้แบบสอบถามที่เกี่ยวข้องกับการร้องขอ

หลังจาก WordPress แยกวิเคราะห์ URL แล้วมันจะตั้งค่าคลาสเคียวรีหลักWP_Queryซึ่งทำในmain()วิธีเดียวกันกับWPคลาส WP_Queryสามารถพบเนื้อวัวของในget_posts()วิธีการที่มีการแยกวิเคราะห์ข้อโต้แย้งและ sanitized และแบบสอบถาม SQL ที่เกิดขึ้นจริง (และในที่สุดก็เรียกใช้)

ในวิธีนี้ในบรรทัดที่ 2730 รหัสต่อไปนี้จะถูกดำเนินการ:

$q['name'] = sanitize_title_for_query( $q['name'] );

วิธีนี้จะทำให้โพสต์นั้นถูกดึงออกมาจากตารางโพสต์ การส่งออกข้อมูลการดีบักภายในลูปแสดงว่านี่คือที่ที่ปัญหาอยู่: ชื่อโพสต์ของคุณmy-permalink~จะถูกเปลี่ยนเป็นmy-permalinkซึ่งจะใช้ในการดึงข้อมูลการโพสต์จากฐานข้อมูล

ฟังก์ชั่นการโพสต์ชื่อการฆ่าเชื้อ

ฟังก์ชั่นการsanitize_title_for_queryโทรsanitize_titleด้วยพารามิเตอร์ที่เหมาะสมซึ่งดำเนินการเพื่อให้สะอาดชื่อ ตอนนี้แก่นของฟังก์ชั่นนี้คือการใช้sanitize_titleตัวกรอง:

$title = apply_filters( 'sanitize_title', $title, $raw_title, $context );

ตัวกรองนี้ได้ใน WordPress sanitize_title_with_dashesพื้นเมืองฟังก์ชันเดียวที่ติดอยู่กับมัน ผมเคยเขียนภาพรวมที่กว้างขวางของสิ่งที่ฟังก์ชั่นนี้ไม่ซึ่งสามารถพบได้ที่นี่ ในฟังก์ชันนี้บรรทัดที่ทำให้เกิดปัญหาของคุณคือ

$title = preg_replace('/[^%a-z0-9 _-]/', '', $title);

บรรทัดนี้รวมอักขระทั้งหมดยกเว้นอักขระตัวอักษรและตัวเลขช่องว่างเครื่องหมายขีดกลางและเครื่องหมายขีดล่าง

การแก้ปัญหาของคุณ

ดังนั้นจึงเป็นวิธีเดียวที่จะแก้ปัญหาของคุณ: การลบsanitize_title_with_dashesฟังก์ชั่นออกจากตัวกรองและแทนที่ด้วยฟังก์ชั่นของคุณเอง อันที่จริงแล้วไม่ใช่เรื่องยากที่จะทำแต่ :

  1. เมื่อ WordPress เปลี่ยนกระบวนการภายในของการฆ่าเชื้อโรคชื่อเรื่องนี้จะมีผลกระทบสำคัญในเว็บไซต์ของคุณ
  2. ปลั๊กอินอื่น ๆ ที่เกี่ยวกับตัวกรองนี้อาจจัดการฟังก์ชันการทำงานใหม่ไม่ถูกต้อง
  3. สิ่งสำคัญที่สุด : WordPress ใช้ผลลัพธ์ของsanitize_titleฟังก์ชันโดยตรงในแบบสอบถาม SQL โดยบรรทัดนี้:

    $where .= " AND $wpdb->posts.post_name = '" . $q['name'] . "'";

    หากคุณต้องการพิจารณาการเปลี่ยนตัวกรองตรวจสอบให้แน่ใจว่าคุณได้ลบชื่อเรื่องอย่างถูกต้องก่อนที่จะใช้ตัวกรอง

สรุป: การแก้ปัญหาของคุณไม่จำเป็นต้องเกี่ยวข้องกับความปลอดภัย แต่ถ้าคุณต้องการที่จะทำมันให้แทนที่sanitize_title_with_dashesด้วยฟังก์ชั่นของคุณเองและใส่ใจกับการหลบหนีของ SQL

หมายเหตุชื่อไฟล์และหมายเลขบรรทัดทั้งหมดสอดคล้องกับไฟล์ WordPress 4.4.2


3

บางคนได้อธิบายปัญหาไปแล้วดังนั้นฉันจะโพสต์ทางเลือกอื่น ควรอธิบายด้วยตนเอง

add_action( 'template_redirect', function() {
    global $wp;

    if ( ! is_singular() || empty( $wp->query_vars['name'] ) )
        return;

    if ( $wp->query_vars['name'] != get_query_var( 'name' ) ) {
        die( wp_redirect( get_permalink(), 301 ) );
        // or 404, or 403, or whatever you want.
    }
});

คุณจะต้องทำอะไรบางอย่างที่แตกต่างกันเล็กน้อยสำหรับโพสต์ประเภทลำดับชั้น แต่เนื่องจากWP_Queryจะวิ่งpagenameผ่านwp_basenameแล้ว sanitize มันดังนั้นquery_vars['pagename']และget_query_var('pagename')จะไม่ตรงกับสำหรับเด็กเพราะหลังจะได้มีส่วนร่วมของผู้ปกครอง

ฉันหวังว่าจะredirect_canonicalดูแลอึนี้


0

นี่คือการแก้ไข ... สำหรับข้อผิดพลาดของ WORDPRESS เพียงเพิ่มบล็อกตัวดัดแปลงความปลอดภัย BEGIN ด้านบนบล็อกที่สร้างโดย Wordpress

# BEGIN security mod
<IfModule mod_rewrite.c>
RewriteRule ^.*[~]+.*$ - [R=404]
</IfModule>
#END security mod

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wordpress/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /wordpress/index.php [L]
</IfModule>

# END WordPress

-3

คุณสามารถลองเพิ่มการเพิ่มลงใน.htaccessไฟล์ของคุณได้ตลอดเวลา:

RewriteEngine On
RewriteRule \.php~$  [forbidden,last]

บรรทัดที่สองด้านบนควรไปทางขวาภายใต้บรรทัดแรกที่แสดง ควรป้องกันไม่ให้index.php~แสดงใน URL


มันไม่ได้ผลสำหรับคำถามที่เกี่ยวข้องกันใช่ไหม?
Nicolai
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.