Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion mordant-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
"reimplemented_helper:src/runtime/api/bun/Terminal.rs" = 1
"reimplemented_helper:src/runtime/hw_exports.rs" = 1
"same_match_twice:src/runtime/api/bun/h2_frame_parser.rs" = 1
"same_match_twice:src/runtime/api/csrf_jsc.rs" = 1
"same_match_twice:src/runtime/bake/production.rs" = 2
"same_match_twice:src/runtime/cli/pack_command.rs" = 1
"same_match_twice:src/runtime/cli/publish_command.rs" = 1
Expand Down
76 changes: 30 additions & 46 deletions src/runtime/api/csrf_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ fn get_optional_int_u64(
Ok(Some(num as u64))
}

/// Reads the optional `encoding` property. Parsed as a Buffer encoding name
/// ("" selects base64url), of which only the three token formats are accepted.
fn get_optional_token_format(
target: JSValue,
global: &JSGlobalObject,
) -> JsResult<Option<csrf::TokenFormat>> {
let Some(value) = target.get(global, "encoding")? else {
return Ok(None);
};
let encoding =
NodeEncoding::from_js_with_default_on_empty(value, global, NodeEncoding::Base64url)?;
let format = match encoding {
Some(NodeEncoding::Base64) => csrf::TokenFormat::Base64,
Some(NodeEncoding::Base64url) => csrf::TokenFormat::Base64Url,
Some(NodeEncoding::Hex) => csrf::TokenFormat::Hex,
_ => {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid format: must be 'base64', 'base64url', or 'hex'"
)));
}
};
Ok(Some(format))
}

/// JS binding function for generating CSRF tokens
/// First argument is secret (required), second is options (optional)
#[bun_jsc::host_fn]
Expand Down Expand Up @@ -96,27 +120,8 @@ pub(crate) fn csrf__generate(global: &JSGlobalObject, frame: &CallFrame) -> JsRe
}

// Extract encoding (optional)
if let Some(encoding_js) = options_value.get(global, "encoding")? {
let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty(
encoding_js,
global,
NodeEncoding::Base64url,
)?
else {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid format: must be 'base64', 'base64url', or 'hex'"
)));
};
encoding = match encoding_enum {
NodeEncoding::Base64 => csrf::TokenFormat::Base64,
NodeEncoding::Base64url => csrf::TokenFormat::Base64Url,
NodeEncoding::Hex => csrf::TokenFormat::Hex,
_ => {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid format: must be 'base64', 'base64url', or 'hex'"
)));
}
};
if let Some(format) = get_optional_token_format(options_value, global)? {
encoding = format;
}

if let Some(algorithm_js) = options_value.get(global, "algorithm")? {
Expand Down Expand Up @@ -177,10 +182,8 @@ pub(crate) fn csrf__generate(global: &JSGlobalObject, frame: &CallFrame) -> JsRe
}
};

// Encode the token
// `csrf::TokenFormat::to_node_encoding()` returns the cycle-broken
// `bun_core::NodeEncoding`, not `crate::node::Encoding` (which owns
// `encode_with_max_size`). Map locally to the runtime enum instead.
// Encode the token. `bun_csrf` sits below `crate::node::Encoding` (which
// owns `encode_with_max_size`), so `TokenFormat` is mapped back to it here.
let node_encoding = match encoding {
csrf::TokenFormat::Base64 => NodeEncoding::Base64,
csrf::TokenFormat::Base64Url => NodeEncoding::Base64url,
Expand Down Expand Up @@ -251,27 +254,8 @@ pub(crate) fn csrf__verify(global: &JSGlobalObject, frame: &CallFrame) -> JsResu
}

// Extract encoding (optional)
if let Some(encoding_js) = options_value.get(global, "encoding")? {
let Some(encoding_enum) = NodeEncoding::from_js_with_default_on_empty(
encoding_js,
global,
NodeEncoding::Base64url,
)?
else {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid format: must be 'base64', 'base64url', or 'hex'"
)));
};
encoding = match encoding_enum {
NodeEncoding::Base64 => csrf::TokenFormat::Base64,
NodeEncoding::Base64url => csrf::TokenFormat::Base64Url,
NodeEncoding::Hex => csrf::TokenFormat::Hex,
_ => {
return Err(global.throw_invalid_arguments(format_args!(
"Invalid format: must be 'base64', 'base64url', or 'hex'"
)));
}
};
if let Some(format) = get_optional_token_format(options_value, global)? {
encoding = format;
}
if let Some(algorithm_js) = options_value.get(global, "algorithm")? {
if !algorithm_js.is_string() {
Expand Down
17 changes: 17 additions & 0 deletions test/js/bun/util/csrf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,23 @@ describe("Bun.CSRF", () => {
expect(() => CSRF.verify(token, { secret, sessionId: 123 })).toThrow();
});

test("rejects encodings that are not token formats", () => {
const token = CSRF.generate(secret);
const message = "Invalid format: must be 'base64', 'base64url', or 'hex'";

// Buffer encodings that are not token formats, and names that are not encodings at all
for (const encoding of ["utf8", "latin1", "buffer", "bogus"]) {
// @ts-expect-error - testing invalid input
expect(() => CSRF.generate(secret, { encoding })).toThrow(message);
// @ts-expect-error - testing invalid input
expect(() => CSRF.verify(token, { secret, encoding })).toThrow(message);
}

// An empty string selects the default (base64url) in both directions
// @ts-expect-error - testing invalid input
expect(CSRF.verify(CSRF.generate(secret, { encoding: "" }), { secret, encoding: "" })).toBe(true);
});

test("handle bad decoding", () => {
const ambigousSecret = "test-secret";

Expand Down
Loading