Skip to content

Commit

Permalink
Merge pull request #682 from CloudCannon/fix/anchor-leakage
Browse files Browse the repository at this point in the history
Fix issue where anchors and weights will escape through meta and filters
  • Loading branch information
bglw authored Aug 15, 2024
2 parents ae03bb2 + fd3c077 commit 7c66b6d
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 37 deletions.
31 changes: 15 additions & 16 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

env:
CARGO_TERM_COLOR: always
HUMANE_VERSION: "0.9.1"
HUMANE_VERSION: "0.9.0"
WASM_PACK_VERSION: "v0.10.3"

jobs:
Expand All @@ -29,14 +29,14 @@ jobs:
~/.rustup
target
key: ${{ runner.os }}-stable-min165

- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
default: true
components: rustfmt, clippy
toolchain: stable
override: true
default: true
components: rustfmt, clippy

- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
Expand All @@ -54,7 +54,7 @@ jobs:
git checkout -b deploy_branch
- name: Prepare Crates
run: |
# Update cargo version,
# Update cargo version,
node ./.backstage/version.cjs
git add ./pagefind/Cargo.toml
git add ./pagefind_web/Cargo.toml
Expand Down Expand Up @@ -398,19 +398,19 @@ jobs:
if: matrix.target == 'aarch64-unknown-linux-musl'
run: |
sudo apt update -y
sudo apt install -y clang gcc-aarch64-linux-gnu
sudo apt install -y clang gcc-aarch64-linux-gnu
echo "TARGET_CC=clang" >> $GITHUB_ENV
echo "CFLAGS_aarch64_unknown_linux_musl=--sysroot=/usr/aarch64-linux-gnu" >> $GITHUB_ENV
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/usr/aarch64-linux-gnu/bin/ld" >> $GITHUB_ENV
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
default: true
components: rustfmt, clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
default: true
components: rustfmt, clippy

- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
Expand All @@ -434,7 +434,7 @@ jobs:
git checkout -b deploy_branch
- name: Prepare Crates
run: |
# Update cargo version,
# Update cargo version,
node ./.backstage/version.cjs
git add ./pagefind/Cargo.toml
git add ./pagefind_web/Cargo.toml
Expand Down Expand Up @@ -486,7 +486,6 @@ jobs:
working-directory: ./pagefind
run: cargo build --release --target ${{ matrix.target }}


- name: Create Standard Release
run: |
EXEC_NAME="pagefind"
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

env:
CARGO_TERM_COLOR: always
HUMANE_VERSION: "0.9.1"
HUMANE_VERSION: "0.9.0"
WASM_PACK_VERSION: "v0.10.3"

jobs:
Expand Down Expand Up @@ -59,11 +59,11 @@ jobs:
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
default: true
components: rustfmt, clippy
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
default: true
components: rustfmt, clippy

- name: Check versions
run: |
Expand Down
65 changes: 65 additions & 0 deletions pagefind/features/edge_cases.feature
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,68 @@ Feature: Graceful Pagefind Errors
"""
Then There should be no logs
Then The selector "[data-url]" should contain "/test/#%D8%A7%D8%B2, /test/#_top, /test/#rtl-content"

Scenario: Anchors do not leak through metadata
Given I have a "public/index.html" file with the content:
"""
<!DOCTYPE html>
<html>
<body>
<p data-title>Nothing</p>
<p data-subs>Nothing</p>
<p data-meta>Nothing</p>
<p data-filter>Nothing</p>
</body>
</html>
"""
Given I have a "public/test/index.html" file with the content:
"""
<!DOCTYPE html>
<html>
<body>
<h1 id="heading_id">
<a href="#heading_id" id="heading_id">Heading text</a>
</h1>
<h2 id="second_heading_id">
<a href="#ack" id="ack">Second meta text</a>
</h2>
<p data-pagefind-meta="extra_meta">
<a href="#meta_id" id="meta_id">Extra meta text</a>
</p>
<p data-pagefind-filter="extra_filter">
<a href="#filter_id" id="filter_id">Extra filter text</a>
</p>
</body>
</html>
"""
When I run my program
Then I should see "Running Pagefind" in stdout
Then I should see the file "public/pagefind/pagefind.js"
When I serve the "public" directory
When I load "/"
When I evaluate:
"""
async function() {
let pagefind = await import("/pagefind/pagefind.js");
let search = await pagefind.search("text");
let results = await Promise.all(search.results.map(r => r.data()));
let result = results[0];
document.querySelector('[data-title]').innerText = result.meta.title;
let subs = result.sub_results.map(s => s.title).sort().join(', ');
document.querySelector('[data-subs]').innerText = subs;
document.querySelector('[data-meta]').innerText = result.meta.extra_meta;
let filters = await pagefind.filters();
document.querySelector('[data-filter]').innerText = Object.keys(filters.extra_filter).join(", ");
}
"""
Then There should be no logs
Then The selector "[data-title]" should contain "Heading text"
Then The selector "[data-subs]" should contain "Heading text, Second meta text"
Then The selector "[data-meta]" should contain "Extra meta text"
Then The selector "[data-filter]" should contain "Extra filter text"
55 changes: 42 additions & 13 deletions pagefind/src/fossick/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ lazy_static! {
static ref NEWLINES: Regex = Regex::new("(\n|\r\n)+").unwrap();
static ref TRIM_NEWLINES: Regex = Regex::new("^[\n\r\\s]+|[\n\r\\s]+$").unwrap();
static ref EXTRANEOUS_SPACES: Regex = Regex::new("\\s{2,}").unwrap();
static ref PRIVATE_PAGEFIND: Regex = Regex::new("___PAGEFIND_[\\S]+\\s?").unwrap();
// TODO: i18n?
static ref SPECIAL_CHARS: Regex = Regex::new("[^\\w]").unwrap();
}
Expand Down Expand Up @@ -108,7 +109,9 @@ impl Fossicker {
}

async fn read_file(&mut self, options: &SearchOptions) -> Result<(), Error> {
let Some(file_path) = &self.file_path else { return Ok(()) }; // TODO: Change to thiserror
let Some(file_path) = &self.file_path else {
return Ok(());
}; // TODO: Change to thiserror
let file = File::open(file_path).await?;

let mut rewriter = DomParser::new(options);
Expand Down Expand Up @@ -162,7 +165,9 @@ impl Fossicker {
}

async fn read_synthetic(&mut self, options: &SearchOptions) -> Result<(), Error> {
let Some(contents) = self.synthetic_content.as_ref() else { return Ok(()) };
let Some(contents) = self.synthetic_content.as_ref() else {
return Ok(());
};

let mut rewriter = DomParser::new(options);

Expand Down Expand Up @@ -419,6 +424,29 @@ impl Fossicker {
(content, map, anchors, max_word_index + 1)
}

/// Removes private Pagefind sentinel values from content that would otherwise leak.
/// This should probably be handled better by not inserting these flags here in the first place,
/// though there's a chance we do want to process them when we arrive at indexing metadata.
fn tidy_meta_and_filters(&mut self) {
if let Some(data) = self.data.as_mut() {
for filter in data.filters.values_mut() {
for filter_val in filter.iter_mut() {
match PRIVATE_PAGEFIND.replace_all(filter_val, "") {
std::borrow::Cow::Borrowed(_) => { /* no-op, no replace happened */ }
std::borrow::Cow::Owned(s) => *filter_val = s,
}
}
}

for meta in data.meta.values_mut() {
match PRIVATE_PAGEFIND.replace_all(meta, "") {
std::borrow::Cow::Borrowed(_) => { /* no-op, no replace happened */ }
std::borrow::Cow::Owned(s) => *meta = s,
}
}
}
}

async fn fossick_html(&mut self, options: &SearchOptions) {
if self.synthetic_content.is_some() {
while self.read_synthetic(options).await.is_err() {
Expand All @@ -437,6 +465,7 @@ impl Fossicker {
};

let (content, word_data, anchors, word_count) = self.parse_digest();
self.tidy_meta_and_filters();

let data = self.data.unwrap();
let url = if let Some(url) = &self.page_url {
Expand Down Expand Up @@ -896,14 +925,14 @@ mod tests {
let full_zh_input = "___PAGEFIND_AUTO_WEIGHT___7 擁有遠端帳號權限 ___END_PAGEFIND_WEIGHT___
我們建議大多數具有遠端帳號權限的使用者,採用 ___PAGEFIND_ANCHOR___a:0:my-link Certbot 這個 ACME 客戶端。它可以自動執行憑證的頒發、安裝,甚至不需要停止你的伺服器;Certbot 也提供專家模式,給不想要自動設定的使用者。Certbot 操作簡單,適用於許多系統;並且具有完善的文檔。參考 Certbot 官網,以獲取對於不同系統和網頁伺服器的操作說明。
如果 Certbot 不能滿足你的需求,或是你想嘗試別的客戶端,還有很多 ACME 用戶端可供選擇。在你選定 ACME 客戶端軟體後,請參閱該客戶端的文檔。
___PAGEFIND_WEIGHT___44
如果你正在嘗試使用不同的 ACME 用戶端,請使用我們的測試環境以免超過憑證頒發與更新的速率限制。
沒有遠端帳號權限
在沒有遠端帳號權限的情況下,最好的辦法是使用服務業者所提供的現有支援。如果你的業者支援 ___PAGEFIND_ANCHOR___a:1:my-second-link Let’s Encrypt,那麼他們就能幫助你申請免費憑證;安裝並設定自動更新。某些業者會需要你在控制介面或聯繫客服以開啟 Let’s Encrypt 服務。也有些業者會為所有客戶自動設定並安裝憑證。
查看支援 Let’s Encrypt 的業者列表,確認你提供商的是否有出現在列表上。如果有的話,請按照他們的文檔設定 Let’s Encrypt 憑證。 ___END_PAGEFIND_WEIGHT___";

let (legitimate_zh_output, chunked_zh_output) =
Expand All @@ -915,14 +944,14 @@ mod tests {
let full_zh_cn_input = "没有命令行访问权限
在没有命令行访问权限的情况下,___PAGEFIND_AUTO_WEIGHT___7 最好的办法是使用您托管服务提供商提供的内置功能。 支持 Let’s Encrypt 的服务商能替您自动完成免费证书的申请、安装、续期步骤。 某些服务商可能需要您在控制面板中开启相关选项, 也有一些服务商会自动为所有客户申请并安装证书。
如果您的服务商存在于我们的服务商列表中, 参照其文档设置 Let’s Encrypt ___END_PAGEFIND_WEIGHT___ 证书即可。
如果您的托管服务提供商不支持 ___PAGEFIND_ANCHOR___a:0:my-link Let’s Encrypt,您可以与他们联系请求支持。 我们尽力使添加 Let’s Encrypt 支持变得非常容易,提供商(注:非中国国内提供商)通常很乐意听取客户的建议!
如果您的托管服务提供商不想集成 Let’s Encrypt,但支持上传自定义证书,您可以在自己的计算机上安装 Certbot 并使用手动模式(Manual Mode)。 在手动模式下,您需要将指定文件上传到您的网站以证明您的控制权。 然后,Certbot 将获取您可以上传到提供商的证书。 我们不建议使用此选项,因为它非常耗时,并且您需要在证书过期时重复此步骤。 对于大多数人来说,最好从提供商处请求 Let’s Encrypt 支持。若您的提供商不打算兼容,建议您更换提供商。
获取帮助
如果您对选择 ACME 客户端,使用特定客户端或与 Let’s Encrypt 相关的任何其他内容有疑问,请前往我们的社区论坛获取帮助。";

let (legitimate_zh_cn_output, chunked_zh_cn_output) =
Expand All @@ -934,14 +963,14 @@ mod tests {
let full_ja_input = "___PAGEFIND_AUTO_WEIGHT___7 シェルへのアクセス権を持っている場合
シェルアクセスができるほとんどの人には、Certbot という ACME クライアントを使うのがおすすめです。 ___END_PAGEFIND_WEIGHT___ 証明書の発行とインストールを、ダウンタイムゼロで自動化できます。 自動設定を使いたくない人のために、エキスパートモードも用意されています。 とても簡単に使え、多数のオペレーティングシステムで動作し、たくさんのドキュメントもあります。 Certbot のウェブサイトでは、各オペレーティングシステムやウェブサーバーごとの個別の設定方法について解説されています。
Certbot があなたの要件を満たさない場合や、他のクライアントを試してみたい場合には、Certbot の他にもたくさんの ACME クライアントが利用できます。 ACME クライアントを自分で選んだ場合は、そのクライアントのドキュメントを参照してください。
別の ACME クライアントを使って実験を行う場合は、 ___PAGEFIND_ANCHOR___a:0:my-link 私たちが用意したステージング環境を利用して、レート・リミットの制限を受けないように気をつけてください。
シェルへのアクセス権を持っていない場合
シェルアクセスができない場合に Let’s Encrypt を利用する一番良い方法は、ホスティング・プロバイダが用意したサポートを利用することです。 もし、あなたが利用するホスティング・プロバイダが Let’s Encrypt をサポートしている場合、あなたの代わりに無料の証明書をリクエスト、インストールし、自動的に最新の状態に更新してくれます。 一部のホスティング・プロバイダでは、この機能は自分で設定から有効にする必要がある場合があります。 それ以外のプロバイダでは、すべてのユーザーのために、自動で証明書が発行・インストールされるようになっています。
あなたが利用しているホスティング・プロバイダが Let’s Encrypt をサポートしているかどうかは、 ホスティング・プロバイダのリストで確認してください。 もしサポートされている場合は、ホスティング・プロバイダのドキュメンに書かれている Let’s Encrypt の設定方法に従ってください。";

let (legitimate_ja_output, chunked_ja_output) =
Expand Down
4 changes: 2 additions & 2 deletions pagefind/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

cargo build --release --features extended
if [ -z "$1" ]; then
TEST_BINARY=../target/release/pagefind npx -y humane@latest
TEST_BINARY=../target/release/pagefind npx -y humane@0.9.0
else
TEST_BINARY=../target/release/pagefind npx -y humane@latest --name "$1"
TEST_BINARY=../target/release/pagefind npx -y humane@0.9.0 --name "$1"
fi

0 comments on commit 7c66b6d

Please sign in to comment.